UDP + broadcast support:
- Support for broadcast (discovering all devices and mass commands) - Support for UDP communication - Configurable UDP timeout and source address (for both TCP and UDP) - Only json output sent to stdout, rest to stderr, to get clean json output
This commit is contained in:
parent
395c352100
commit
2c1347b565
15
README.md
15
README.md
@ -5,7 +5,8 @@ For the full story, see [Reverse Engineering the TP-Link HS110](https://www.soft
|
|||||||
## tplink_smartplug.py ##
|
## tplink_smartplug.py ##
|
||||||
|
|
||||||
A python client for the proprietary TP-Link Smart Home protocol to control TP-Link HS100 and HS110 WiFi Smart Plugs.
|
A python client for the proprietary TP-Link Smart Home protocol to control TP-Link HS100 and HS110 WiFi Smart Plugs.
|
||||||
The SmartHome protocol runs on TCP port 9999 and uses a trivial XOR autokey encryption that provides no security.
|
The SmartHome protocol runs on TCP/UDP port 9999 and uses a trivial XOR autokey encryption that provides no security.
|
||||||
|
Other TP-Link products use the same protocol (such as LB-XXX lightbulbs), but may not be compatible with command templates like `on` or `off`.
|
||||||
|
|
||||||
There is no authentication mechanism and commands are accepted independent of device state (configured/unconfigured).
|
There is no authentication mechanism and commands are accepted independent of device state (configured/unconfigured).
|
||||||
|
|
||||||
@ -23,9 +24,14 @@ A full list of commands is provided in [tplink-smarthome-commands.txt](tplink-sm
|
|||||||
|
|
||||||
#### Usage ####
|
#### Usage ####
|
||||||
|
|
||||||
`./tplink_smartplug.py -t <ip> [-c <cmd> || -j <json>]`
|
`./tplink_smartplug.py [-h] (-t <hostname> | -b)
|
||||||
|
[-c <command> | -j <JSON string>] [-u]
|
||||||
|
[-s <address>] [-T <seconds>]`
|
||||||
|
|
||||||
Provide the target IP using `-t` and a command to send using either `-c` or `-j`. Commands for the `-c` flag:
|
|
||||||
|
Provide the target IP using `-t` and a command to send using either `-c` or `-j`. Use `-u` to use UDP instead of TCP, and use `-b` to send to the 255.255.255.255 broadcast address (also UDP). To send from a specific source address, specify the address with `-s`. To specify a timeout to wait for all devices to reply to a broadcast, use `-T` (also applies to UDP, default 0.5 seconds). When sending a broadcast, the script will dump all responses received and wait until timeout.
|
||||||
|
|
||||||
|
Predefined commands for the `-c` flag:
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|-----------|--------------------------------------|
|
|-----------|--------------------------------------|
|
||||||
@ -42,6 +48,9 @@ Provide the target IP using `-t` and a command to send using either `-c` or `-j`
|
|||||||
| reset | Reset the device to factory settings |
|
| reset | Reset the device to factory settings |
|
||||||
| energy | Return realtime voltage/current/power|
|
| energy | Return realtime voltage/current/power|
|
||||||
|
|
||||||
|
(when no command and no JSON string specified, `info` is used by default)
|
||||||
|
|
||||||
|
|
||||||
More advanced commands such as creating or editing rules can be issued using the `-j` flag by providing the full JSON string for the command. Please consult [tplink-smarthome-commands.txt](tplink-smarthome-commands.txt) for a comprehensive list of commands.
|
More advanced commands such as creating or editing rules can be issued using the `-j` flag by providing the full JSON string for the command. Please consult [tplink-smarthome-commands.txt](tplink-smarthome-commands.txt) for a comprehensive list of commands.
|
||||||
|
|
||||||
## Wireshark Dissector ##
|
## Wireshark Dissector ##
|
||||||
|
@ -4,7 +4,8 @@
|
|||||||
# For use with TP-Link HS-100 or HS-110
|
# For use with TP-Link HS-100 or HS-110
|
||||||
#
|
#
|
||||||
# by Lubomir Stroetmann
|
# by Lubomir Stroetmann
|
||||||
# Copyright 2016 softScheck GmbH
|
# Copyright 2016-2018 softScheck GmbH
|
||||||
|
# Copyrifht 2018 Wojciech Owczarek
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@ -19,17 +20,18 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
|
|
||||||
import socket
|
from socket import *
|
||||||
import argparse
|
|
||||||
from struct import pack
|
from struct import pack
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
version = 0.2
|
version = 0.3
|
||||||
|
|
||||||
# Check if hostname is valid
|
# Check if hostname is valid
|
||||||
def validHostname(hostname):
|
def validHostname(hostname):
|
||||||
try:
|
try:
|
||||||
socket.gethostbyname(hostname)
|
gethostbyname(hostname)
|
||||||
except socket.error:
|
except error:
|
||||||
parser.error("Invalid hostname.")
|
parser.error("Invalid hostname.")
|
||||||
return hostname
|
return hostname
|
||||||
|
|
||||||
@ -53,7 +55,10 @@ commands = {'info' : '{"system":{"get_sysinfo":{}}}',
|
|||||||
# XOR Autokey Cipher with starting key = 171
|
# XOR Autokey Cipher with starting key = 171
|
||||||
def encrypt(string):
|
def encrypt(string):
|
||||||
key = 171
|
key = 171
|
||||||
result = pack('>I', len(string))
|
if args.broadcast or args.udp:
|
||||||
|
result = ""
|
||||||
|
else:
|
||||||
|
result = pack('>I', len(string))
|
||||||
for i in string:
|
for i in string:
|
||||||
a = key ^ ord(i)
|
a = key ^ ord(i)
|
||||||
key = a
|
key = a
|
||||||
@ -71,33 +76,82 @@ def decrypt(string):
|
|||||||
|
|
||||||
# Parse commandline arguments
|
# Parse commandline arguments
|
||||||
parser = argparse.ArgumentParser(description="TP-Link Wi-Fi Smart Plug Client v" + str(version))
|
parser = argparse.ArgumentParser(description="TP-Link Wi-Fi Smart Plug Client v" + str(version))
|
||||||
parser.add_argument("-t", "--target", metavar="<hostname>", required=True, help="Target hostname or IP address", type=validHostname)
|
|
||||||
group = parser.add_mutually_exclusive_group(required=True)
|
group = parser.add_mutually_exclusive_group(required=True)
|
||||||
|
group.add_argument("-t", "--target", metavar="<hostname>", help="Target hostname or IP address", type=validHostname)
|
||||||
|
group.add_argument("-b", "--broadcast", help="Send UDP broadcast (255.255.255.255)", default=False, action='store_true')
|
||||||
|
|
||||||
|
group = parser.add_mutually_exclusive_group()
|
||||||
group.add_argument("-c", "--command", metavar="<command>", help="Preset command to send. Choices are: "+", ".join(commands), choices=commands)
|
group.add_argument("-c", "--command", metavar="<command>", help="Preset command to send. Choices are: "+", ".join(commands), choices=commands)
|
||||||
group.add_argument("-j", "--json", metavar="<JSON string>", help="Full JSON string of command to send")
|
group.add_argument("-j", "--json", metavar="<JSON string>", help="Full JSON string of command to send")
|
||||||
|
|
||||||
|
parser.add_argument("-u", "--udp", help="Send command via UDP instead of TCP (broadcast always UDP)", default=False, action='store_true')
|
||||||
|
parser.add_argument("-s", "--source", metavar="<address>", help="Source IP address to use (default is any source)", default="0.0.0.0", type=validHostname)
|
||||||
|
parser.add_argument("-T", "--timeout", metavar="<seconds>", help="Maximum time to wait for broadcast reply", default="0.5", type=float, choices=range(0, 3600))
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
# Set target IP, port and command to send
|
# Set target IP, port and command to send
|
||||||
|
bufsize = 2048
|
||||||
ip = args.target
|
ip = args.target
|
||||||
|
listenport = 9999
|
||||||
port = 9999
|
port = 9999
|
||||||
|
headerlen = 4
|
||||||
|
|
||||||
if args.command is None:
|
if args.command is None:
|
||||||
cmd = args.json
|
cmd = args.json
|
||||||
|
if cmd is None:
|
||||||
|
cmd = commands['info']
|
||||||
else:
|
else:
|
||||||
cmd = commands[args.command]
|
cmd = commands[args.command]
|
||||||
|
|
||||||
|
if args.broadcast:
|
||||||
|
ip = "255.255.255.255"
|
||||||
|
headerlen = 0
|
||||||
|
if args.udp:
|
||||||
|
headerlen = 0
|
||||||
|
|
||||||
# Send command and receive reply
|
# Send command and receive reply
|
||||||
try:
|
try:
|
||||||
sock_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
sock_tcp.connect((ip, port))
|
|
||||||
sock_tcp.send(encrypt(cmd))
|
|
||||||
data = sock_tcp.recv(2048)
|
|
||||||
sock_tcp.close()
|
|
||||||
|
|
||||||
print "Sent: ", cmd
|
gotdata = False
|
||||||
print "Received: ", decrypt(data[4:])
|
|
||||||
except socket.error:
|
if args.broadcast or args.udp:
|
||||||
|
sock = socket(AF_INET, SOCK_DGRAM)
|
||||||
|
sock.bind((args.source, listenport))
|
||||||
|
sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
|
||||||
|
if args.broadcast:
|
||||||
|
sock.setsockopt(SOL_SOCKET, SO_BROADCAST, 1)
|
||||||
|
sock.sendto(encrypt(cmd), (ip, port))
|
||||||
|
if args.broadcast:
|
||||||
|
sys.stderr.write("Broadcasted: " + cmd + '\n')
|
||||||
|
else:
|
||||||
|
sys.stderr.write("Sent via UDP: " + cmd + '\n')
|
||||||
|
sock.settimeout(args.timeout)
|
||||||
|
(data, src) = sock.recvfrom(bufsize)
|
||||||
|
while data is not None:
|
||||||
|
gotdata = True
|
||||||
|
sys.stderr.write("Received from "+src[0]+":"+str(src[1])+": ")
|
||||||
|
print decrypt(data[headerlen:])
|
||||||
|
(data, src) = sock.recvfrom(bufsize)
|
||||||
|
else:
|
||||||
|
sock = socket(AF_INET, SOCK_STREAM)
|
||||||
|
sock.bind((args.source, listenport))
|
||||||
|
sock.connect((ip, port))
|
||||||
|
sock.send(encrypt(cmd))
|
||||||
|
sys.stderr.write("Sent via TCP: " + cmd + '\n')
|
||||||
|
data = sock.recv(bufsize)
|
||||||
|
sys.stderr.write("Received: ")
|
||||||
|
print decrypt(data[headerlen:])
|
||||||
|
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
except timeout:
|
||||||
|
if gotdata:
|
||||||
|
quit("Timeout (no more data)")
|
||||||
|
else:
|
||||||
|
quit("Timeout and no data received while sending to " + ip + ":" + str(port))
|
||||||
|
except error:
|
||||||
quit("Cound not connect to host " + ip + ":" + str(port))
|
quit("Cound not connect to host " + ip + ":" + str(port))
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user