Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UDP + broadcast support #45

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand All @@ -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 |
|-----------|--------------------------------------|
Expand All @@ -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 ##
Expand Down
90 changes: 72 additions & 18 deletions tplink_smartplug.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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

Expand All @@ -53,7 +55,10 @@ def validHostname(hostname):
# XOR Autokey Cipher with starting key = 171
def encrypt(string):
key = 171
result = pack('>I', len(string))
if args.broadcast or args.udp:
result = ""
else:
result = pack('>I', len(string))
for i in string:
a = key ^ ord(i)
key = a
Expand All @@ -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))