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:
Wojciech Owczarek 2018-07-09 16:40:26 +01:00
parent 395c352100
commit 2c1347b565
2 changed files with 83 additions and 20 deletions

View File

@ -5,7 +5,8 @@ For the full story, see [Reverse Engineering the TP-Link HS110](https://www.soft
## tplink_smartplug.py ##
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).
@ -23,9 +24,14 @@ A full list of commands is provided in [tplink-smarthome-commands.txt](tplink-sm
#### 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 |
|-----------|--------------------------------------|
@ -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 |
| 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.
## Wireshark Dissector ##

View File

@ -4,7 +4,8 @@
# For use with TP-Link HS-100 or HS-110
#
# 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");
# you may not use this file except in compliance with the License.
@ -19,17 +20,18 @@
# limitations under the License.
#
import socket
import argparse
from socket import *
from struct import pack
import argparse
import sys
version = 0.2
version = 0.3
# Check if hostname is valid
def validHostname(hostname):
try:
socket.gethostbyname(hostname)
except socket.error:
gethostbyname(hostname)
except error:
parser.error("Invalid hostname.")
return hostname
@ -53,6 +55,9 @@ commands = {'info' : '{"system":{"get_sysinfo":{}}}',
# XOR Autokey Cipher with starting key = 171
def encrypt(string):
key = 171
if args.broadcast or args.udp:
result = ""
else:
result = pack('>I', len(string))
for i in string:
a = key ^ ord(i)
@ -71,33 +76,82 @@ def decrypt(string):
# Parse commandline arguments
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.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("-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()
# Set target IP, port and command to send
bufsize = 2048
ip = args.target
listenport = 9999
port = 9999
headerlen = 4
if args.command is None:
cmd = args.json
if cmd is None:
cmd = commands['info']
else:
cmd = commands[args.command]
if args.broadcast:
ip = "255.255.255.255"
headerlen = 0
if args.udp:
headerlen = 0
# Send command and receive reply
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
print "Received: ", decrypt(data[4:])
except socket.error:
gotdata = False
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))