diff --git a/README.md b/README.md index d82ac41..6d3a2b3 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,16 @@ # Plex Duplicate Stream Killer -Automatically ban users who share their Plex account with others. **This requires an active [PlexPass](https://www.plex.tv/plex-pass/) subscription to work.** + +Automatically ban users who share their Plex account with others using two detection methods. **This requires an active [PlexPass](https://www.plex.tv/plex-pass/) subscription to work.** # What It Does -This script will query your Plex server every 10 seconds (default) and get a list of current streams. It will automatically kill all streams for a user if two or more of their streams are coming from different IP addresses. + +This script will query your Plex server every 30 seconds (configurable) and get a list of current streams. It employs two methods to detect account sharing: + +1. Real-time IP address analysis: The script will automatically kill all streams for a user if two (configurable) or more of their streams are coming from different IP addresses. +2. Historical IP address analysis (optional): The script can also track user streaming history and ban a user if they have used more than a set limit of IP addresses over a specified time period. # Requirements + - Docker - Docker Compose - Active [PlexPass](https://www.plex.tv/plex-pass/) subscription @@ -13,18 +19,26 @@ This script will query your Plex server every 10 seconds (default) and get a lis ### Example 1 - John has three streams: - - **Stream 1:** IP Address 1.2.3.4 - - **Stream 2:** IP Address 1.2.3.4 - - **Stream 3:** IP Address: 9.8.7.6 - - The script will see that John's account is being used from two unique locations (two IP addresses). All of John's streams will be killed and John will be added to the ban list for 48 hours (default). All of John's attempts to stream will be blocked until his ban has expired. - - ### Example 2 - Mary has two streams: - - - **Stream 1:** IP Address 1.2.3.4 - - **Stream 2:** IP Address 1.2.3.4 - - The script will see that Mary's account is being used from only one unique IP address. Unless Mary's already banned, she'll be allowed to keep streaming. +- **Stream 1:** IP Address 1.2.3.4 +- **Stream 2:** IP Address 1.2.3.4 +- **Stream 3:** IP Address: 9.8.7.6 + +The script will see that John's account is being used from two unique locations (two IP addresses) using the real-time IP address analysis. All of John's streams will be killed, and John will be added to the ban list for 48 hours (default). All of John's attempts to stream will be blocked until his ban has expired. + +### Example 2 - Mary has two streams: + +- **Stream 1:** IP Address 1.2.3.4 +- **Stream 2:** IP Address 1.2.3.4 + +The script will see that Mary's account is being used from only one unique IP address. Unless Mary's already banned or has too many unique IP addresses in her streaming history (if historical IP address analysis is enabled), she'll be allowed to keep streaming. + +### Example 3 - Alice has one stream: + +- **Stream 1:** IP Address 1.2.3.4 + +Alice has only one active stream, so she is not flagged by the real-time IP address analysis. However, if history-based banning is enabled and Alice has used more unique IP addresses than allowed within the specified time period, she will be banned. + +In this case, let's say Alice has streamed from five different IP addresses in the last 12 hours, exceeding the allowed limit. As a result, Alice will be added to the ban list for 48 hours. All her attempts to stream will be blocked until her ban has expired. # Docker Compose Example @@ -33,21 +47,39 @@ This script will query your Plex server every 10 seconds (default) and get a lis version: "3.8" services: dupstreamkiller: - image: ghcr.io/andrewpaglusch/plex-duplicate-stream-killer:v2 - container_name: dupstreamkiller - restart: always - volumes: - - ./plex-duplicate-stream-killer/data:/data - environment: - LOOP_DELAY_SECONDS: 10 - MAX_UNIQUE_STREAMS: 1 - BAN_LENGTH_HRS: 48 - BAN_MSG: YOU HAVE BEEN BANNED FROM PLEX FOR 48 HOURS FOR ACCOUNT SHARING. This is an automated message. - USER_WHITELIST: joeuser55 bobross123 - NETWORK_WHITELIST: 10.15.16.0/24 192.168.0.0/16 - PLEX_URL: http://my-plex-server:32400 - PLEX_TOKEN: myplextokenhere - TELEGRAM_BOT_KEY: 123456789:foobarbizbazfoobarbizbaz - TELEGRAM_CHAT_ID: -123456789 + image: ghcr.io/andrewpaglusch/plex-duplicate-stream-killer:v2 + container_name: dupstreamkiller + restart: always + volumes: + - ./plex-duplicate-stream-killer/data:/data + environment: + LOOP_DELAY_SECONDS: 30 + MAX_UNIQUE_STREAMS: 2 + USER_HISTORY_BAN_ENABLED: true + USER_HISTORY_LENGTH_HRS: 12 + USER_HISTORY_BAN_IP_THRESH: 4 + BAN_LENGTH_HRS: 48 + BAN_MSG: YOU HAVE BEEN BANNED FROM PLEX FOR 48 HOURS FOR ACCOUNT SHARING. This is an automated message. + USER_WHITELIST: joeuser55 bobross123 + NETWORK_WHITELIST: 10.15.16.0/24 192.168.0.0/16 + PLEX_URL: http://my-plex-server:32400 + PLEX_TOKEN: myplextokenhere + TELEGRAM_BOT_KEY: 123456789:foobarbizbazfoobarbizbaz + TELEGRAM_CHAT_ID: -123456789 ``` +# Configuration (Environment Variables) + **All variables are required** + - `LOOP_DELAY_SECONDS`: This is the delay in seconds between each check of the Plex server for active streams. + - `MAX_UNIQUE_STREAMS`: This variable sets the maximum number of unique IP addresses a user is allowed to have for their active streams before being considered for account sharing. + - `USER_HISTORY_BAN_ENABLED`: This variable is a boolean flag that enables or disables the history-based banning feature. Set to "true" to enable history-based banning, and "false" to disable it. On a busy server, this can potentially consume a large amount of memory, especially if `LOOP_DELAY_SECONDS` is set very low, or `USER_HISTORY_LENGHT_HRS` is set very high. + - `USER_HISTORY_LENGTH_HRS`: This variable specifies the duration in hours for which the user's streaming history should be considered when evaluating for history-based banning. + - `USER_HISTORY_BAN_IP_THRESH`: This variable sets the maximum number of unique IP addresses a user is allowed to have in their streaming history within the specified `USER_HISTORY_LENGTH_HRS` before being considered for account sharing. + - `BAN_LENGTH_HRS`: This variable specifies the duration of the ban in hours. Users will be banned for this amount of time if they are flagged for account sharing. + - `BAN_MSG`: This is the message displayed to the user when their streams are killed, and they are banned from the Plex server. + - `USER_WHITELIST`: This is a space-separated list of Plex usernames that are exempt from being checked for account sharing. For example: "joeuser55 bobross123". + - `NETWORK_WHITELIST`: This is a space-separated list of IP addresses or subnets in CIDR notation that are considered "safe" and exempt from account sharing checks. For example: "10.15.16.0/24 192.168.0.0/16". + - `PLEX_URL`: This is the URL of your Plex server, including the protocol (http or https) and port number. For example: "http://my-plex-server:32400". + - `PLEX_TOKEN`: This is your Plex server's authentication token. Replace "myplextokenhere" with your actual token. + - `TELEGRAM_BOT_KEY`: This variable is the API key for your Telegram bot, so you can receive notifications about banned users via Telegram. Replace "123456789:foobarbizbazfoobarbizbaz" with your actual bot API key. + - `TELEGRAM_CHAT_ID`: This variable is the unique identifier for the chat where the bot will send notifications. Replace "-123456789" with your actual chat ID. diff --git a/app/config.ini.TEMPLATE b/app/config.ini.TEMPLATE index cd3e398..2ebde5a 100644 --- a/app/config.ini.TEMPLATE +++ b/app/config.ini.TEMPLATE @@ -1,12 +1,15 @@ [main] loop_delay_seconds: ${LOOP_DELAY_SECONDS} -plex_url: ${PLEX_URL} -plex_token: ${PLEX_TOKEN} +max_unique_streams: ${MAX_UNIQUE_STREAMS} +user_history_ban_enabled: ${USER_HISTORY_BAN_ENABLED} +user_history_length_hrs: ${USER_HISTORY_LENGTH_HRS} +user_history_ban_ip_thresh: ${USER_HISTORY_BAN_IP_THRESH} ban_length_hrs: ${BAN_LENGTH_HRS} ban_msg: ${BAN_MSG} user_whitelist: ${USER_WHITELIST} network_whitelist: ${NETWORK_WHITELIST} -max_unique_streams: ${MAX_UNIQUE_STREAMS} +plex_url: ${PLEX_URL} +plex_token: ${PLEX_TOKEN} [telegram] bot_key: ${TELEGRAM_BOT_KEY} diff --git a/app/run.py b/app/run.py index f56ed7c..6814b0e 100755 --- a/app/run.py +++ b/app/run.py @@ -7,7 +7,6 @@ import time import logging import ipaddress -from pprint import pprint from configparser import ConfigParser def get_streams(plex_url, plex_token): @@ -76,7 +75,7 @@ def _parse_streams(jstreams): stream_data = {'session_id': stream['Session']['id'], 'state': stream['Player']['state'], 'title': stream['title'], - 'device': stream['Player']['device'], + 'device': stream['Player'].get('device', 'Unknown'), 'ip_address': stream['Player']['address']} if username in dreturn.keys(): @@ -109,12 +108,8 @@ def load_bans(): else: logging.debug('Loaded bans from disk') - -def dup_check(user_streams, network_whitelist): - """Returns number of unique ip addresses for user""" - if len(user_streams) == 1: - return 1 - +def get_unique_ips(user_streams, network_whitelist): + """Return list of each IP address being used in user_streams""" ip_address_list = [] for stream in user_streams: # only count streams from non-whitelisted ip addresses @@ -123,9 +118,34 @@ def dup_check(user_streams, network_whitelist): else: ip_address_list.append(stream['ip_address']) - # return count of unique ip addresses for user - return len(list(set(ip_address_list))) - + # return list of unique ip addresses for user streams + return list(set(ip_address_list)) + +def log_user_ip_history(user_history, user, uniq_streams): + """log ip addresses being used by user to user_history, along with timestamp""" + user_history.setdefault(user, []) + time_now = int(time.time()) + user_history[user].extend((time_now, ip) for ip in uniq_streams) + return user_history + +def cleanup_user_history(user_history, user_history_length_hrs): + """remove history that is older than user_history_length_hrs for all users""" + epoch_cutoff = int(time.time()) - (3600 * user_history_length_hrs) + filtered_history = {} + + for user, entries in user_history.items(): + kept_entries = [(epoch, ip_address) for epoch, ip_address in entries if epoch >= epoch_cutoff] + filtered_history[user] = kept_entries + + return filtered_history + +def count_ips_in_history(user_history, user): + """return the number of unique ip addresses a user has logged in user_history""" + if user not in user_history: + return 0 + + uniq_ips = set(ip for _, ip in user_history[user]) + return len(uniq_ips) def ban_user(username, ban_length_hrs, ban_list): """Record username and epoch of ban. Return ban_list with new ban added""" @@ -198,6 +218,9 @@ def telegram_notify(message, telegram_bot_key, chat_id): plex_url = config.get('main', 'plex_url') plex_token = config.get('main', 'plex_token') max_unique_streams = int(config.get('main', 'max_unique_streams')) + user_history_ban_enabled = config.get('main', 'user_history_ban_enabled').lower() == "true" + user_history_length_hrs = int(config.get('main', 'user_history_length_hrs')) + user_history_ban_ip_thresh = int(config.get('main', 'user_history_ban_ip_thresh')) ban_length_hrs = int(config.get('main', 'ban_length_hrs')) ban_msg = config.get('main', 'ban_msg') user_whitelist = config.get('main', 'user_whitelist').lower().split() @@ -214,6 +237,9 @@ def telegram_notify(message, telegram_bot_key, chat_id): # {'bob': EPOCHBANEND, 'joe': 0000000000} ban_list = load_bans() +# {'bob': [(EPOCH1, IPADDR), (EPOCH2, IPADDR),...,] +user_history = {} + try: while True: streams = get_streams(plex_url, plex_token) @@ -237,10 +263,31 @@ def telegram_notify(message, telegram_bot_key, chat_id): save_bans(ban_list) telegram_notify(f"Removed {user} from ban list", telegram_bot_key, telegram_chat_id) - # check to see if user needs to be banned - uniq_stream_locations = dup_check(streams[user], network_whitelist) - if uniq_stream_locations > max_unique_streams: - logging.info(f"Banning user {user} for {ban_length_hrs} hours for streaming from {uniq_stream_locations} unique locations") + #get a unique list of ip addresses that the user is currently streaming with + uniq_streams = get_unique_ips(streams[user], network_whitelist) + + # log user ip history to user_history + if user_history_ban_enabled: + user_history = log_user_ip_history(user_history, user, uniq_streams) + user_history = cleanup_user_history(user_history, user_history_length_hrs) + + # check history to see if too many unique ips have been logged over the past user_history_length_hrs hours + uniq_ip_history = count_ips_in_history(user_history, user) + if uniq_ip_history > user_history_ban_ip_thresh: + logging.info(f"Banning user {user} for streaming from more than {uniq_ip_history} IP addresses over the previous {user_history_ban_ip_thresh} hours") + ban_list = ban_user(user, ban_length_hrs, ban_list) + save_bans(ban_list) + + logging.info(f"Killing all streams for {user}") + kill_all_streams(streams[user], ban_msg + f" Your ban will be lifted in {ban_time_left_human(user, ban_list)}.", plex_url, plex_token) + + telegram_notify(f"Banning user {user} for streaming from more than {uniq_ip_history} IP addresses over the previous {user_history_ban_ip_thresh} hours", telegram_bot_key, telegram_chat_id) + continue + + # check user streams to see if greater than max_unique_streams + uniq_stream_count = len(uniq_streams) + if uniq_stream_count > max_unique_streams: + logging.info(f"Banning user {user} for {ban_length_hrs} hours for streaming from {uniq_stream_count} unique locations. Streams will be killed on next iteration") ban_list = ban_user(user, ban_length_hrs, ban_list) save_bans(ban_list) @@ -248,8 +295,7 @@ def telegram_notify(message, telegram_bot_key, chat_id): log_stream_data(streams[user]) kill_all_streams(streams[user], ban_msg + f" Your ban will be lifted in {ban_time_left_human(user, ban_list)}.", plex_url, plex_token) - telegram_notify(f"Banned {user} for {ban_length_hrs} hours for streaming from {uniq_stream_locations} unique locations.", - telegram_bot_key, telegram_chat_id) + telegram_notify(f"Banned {user} for {ban_length_hrs} hours for streaming from {uniq_stream_count} unique locations.", telegram_bot_key, telegram_chat_id) time.sleep(loop_delay_sec) diff --git a/docker-compose.yml.EXAMPLE b/docker-compose.yml.EXAMPLE deleted file mode 100644 index 6c1991d..0000000 --- a/docker-compose.yml.EXAMPLE +++ /dev/null @@ -1,19 +0,0 @@ -version: "3.5" -services: - dupstreamkiller: - build: . - container_name: dupstreamkiller - restart: always - volumes: - - ./dupstreamkiller:/data - environment: - LOOP_DELAY_SECONDS: 10 - MAX_UNIQUE_STREAMS: 1 - BAN_LENGTH_HRS: 48 - BAN_MSG: YOU HAVE BEEN BANNED FROM PLEX FOR 48 HOURS FOR ACCOUNT SHARING. Please ask @AdminNameHere if you have any questions. This is an automated message. - USER_WHITELIST: - NETWORK_WHITELIST: - PLEX_URL: http://127.0.0.1:10400 - PLEX_TOKEN: ** Plex Token - https://bit.ly/34FeMCo ** - TELEGRAM_BOT_KEY: ** Telegram Bot Key - https://bit.ly/33GhZjV ** - TELEGRAM_CHAT_ID: ** Telegram Group ID - https://bit.ly/374CPeN **