Find-Roku.ps1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
function Find-Roku
{
    <#
    .Synopsis
        Finds Rokus
    .Description
        Finds Rokus on your local area network, using SSDP.
    .Link
        Get-Roku
    .Example
        Find-Roku | Get-Roku
    #>

    [OutputType('Roku.BasicInfo')]
    [CmdletBinding()]
    param(
    # The search timeout, in seconds. Increase this number on
    [Parameter(ValueFromPipelineByPropertyName)]
    [int]$SearchTimeout = 5,

    # If set, will force a rescan of the network.
    # Otherwise, the most recent cached result will be returned.
    [Parameter(ValueFromPipelineByPropertyName)]
    [switch]
    $Force,

    # The type of the device to find. By default, roku:ecp.
    # Changing this value is unlikely to find any Rokus, but you can see other devices with -Verbose.
    [Parameter(ValueFromPipelineByPropertyName)]
    [string]$DeviceType = 'roku:ecp'
    )

    begin {
        #region Embedded C# SSDP Finder
        if (-not ('StartAutomating.RokuFinder' -as [type])) {
Add-Type -TypeDefinition @'
namespace StartAutomating
{
    using System;
    using System.Net;
    using System.Net.Sockets;
    using System.Text;
    using System.Timers;
    using System.Collections.Generic;
 
    public class RokuFinder
    {
        public List<string> FindDevices(string deviceType = "roku:ecp", int searchTimeOut = 5)
        {
            List<string> results = new List<string>();
            const int MaxResultSize = 8096;
            const string MulticastIP = "239.255.255.250";
            const int multicastPort = 1900;
 
            byte[] multiCastData = Encoding.UTF8.GetBytes(string.Format(@"M-SEARCH * HTTP/1.1
HOST: {0}:{1}
MAN: ""ssdp:discover""
MX: {2}
ST: {3}
", MulticastIP, multicastPort, searchTimeOut, deviceType));
 
            Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            socket.SendBufferSize = multiCastData.Length;
            SocketAsyncEventArgs sendEvent = new SocketAsyncEventArgs();
            sendEvent.RemoteEndPoint = new IPEndPoint(IPAddress.Parse(MulticastIP), multicastPort);
            sendEvent.SetBuffer(multiCastData, 0, multiCastData.Length);
            sendEvent.Completed += (sender, e) => {
                if (e.SocketError != SocketError.Success) { return; }
 
                switch (e.LastOperation)
                {
                    case SocketAsyncOperation.SendTo:
                        // When the initial multicast is done, get ready to receive responses
                        e.RemoteEndPoint = new IPEndPoint(IPAddress.Any, 0);
                        byte[] receiveBuffer = new byte[MaxResultSize];
                        socket.ReceiveBufferSize = receiveBuffer.Length;
                        e.SetBuffer(receiveBuffer, 0, MaxResultSize);
                        socket.ReceiveFromAsync(e);
                        break;
 
                    case SocketAsyncOperation.ReceiveFrom:
                        // Got a response, so decode it
                        string result = Encoding.UTF8.GetString(e.Buffer, 0, e.BytesTransferred);
                        if (result.StartsWith("HTTP/1.1 200 OK", StringComparison.InvariantCultureIgnoreCase)) {
                            if (! results.Contains(result)) { results.Add(result); }
                        }
 
                        if (socket != null)// and kick off another read
                            socket.ReceiveFromAsync(e);
                        break;
                    default:
                        break;
                }
            };
 
            Timer t = new Timer(TimeSpan.FromSeconds(searchTimeOut + 1).TotalMilliseconds);
            t.Elapsed += (e, s) => { try { socket.Dispose(); socket = null; } catch {}};
 
            // Kick off the initial Send
            socket.SetSocketOption(SocketOptionLevel.IP,SocketOptionName.MulticastInterface, IPAddress.Parse(MulticastIP).GetAddressBytes());
            socket.SendToAsync(sendEvent);
            t.Start();
            DateTime endTime = DateTime.Now.AddSeconds(searchTimeOut);
            do {
                System.Threading.Thread.Sleep(100);
            } while (DateTime.Now < endTime);
            return results;
        }
    }
}
'@

        }
        #endregion Embedded C# SSDP Finder
    }

    process {
        # If -Force was sent, invalidate the cache
        if ($Force) { $script:CachedDiscoveredRokus = $null }
        if (-not $script:CachedDiscoveredRokus) { # If there is no cache, repopulate it.
            $script:CachedDiscoveredRokus =
                @([StartAutomating.RokuFinder]::new().FindDevices($DeviceType, $SearchTimeout)) |
                    Where-Object {
                        # Write all devices found to Verbose
                        Write-Verbose $_
                        $_ -like '*roku*' # but only pass down devices that could be rokus.
                    } |
                    ForEach-Object {
                        $headerLines = @($_ -split '\r\n') # Split the header lines
                        [PSCustomObject][Ordered]@{
                            # The IPAddress will be within the Location: header
                            IPAddress = $(
                                $(
                                    $headerLines -like 'LOCATION:*' -replace '^Location:\s{1,}'
                                ) -as [uri] # We can force this into a URI
                            ).DnsSafeHost # At which point the DNSSafeHost will be the IP

                            # The serial number is "easier": it's the last part of the USN header
                            SerialNumber = @($headerLines -like 'USN:*' -split ':')[-1]
                            # The Version is a little trickier: It's the first chunk in the SERVER:
                            # header after 'Roku/'
                            Version = @($headerLines -like 'SERVER:*' -split '[\s/]')[2]
                            PSTypeName = 'Roku.BasicInfo'
                        }

                        # Just doing a quick sanity check here
                        # so we don't emit objects we can't accurately map the IP
                        if ($out.IPAddress -like '*.*') {
                            $out.IPAddress = [IPAddress]$out.IPAddress
                            $out
                        }
                    }
        }
        $script:CachedDiscoveredRokus # Output the cached value.
    }
}