-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathresources.js
More file actions
190 lines (169 loc) · 8.32 KB
/
Copy pathresources.js
File metadata and controls
190 lines (169 loc) · 8.32 KB
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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
import { Resource, databases } from 'harper';
const { subscriber_log } = databases.ratelimit;
/**
* Rate Limiting System
* Alias name: VOD Segment Access Control System
* This module handles subscriber log entries and provides the logic
* to prevent bot users from accessing resources (VOD segments).
*/
// Define Piracy check limits
// Time interval in seconds ( Default : 10 seconds)
const TIME_INTERVAL = 10;
// Number of requests for unique combination of subscriberID and Contentname ( Default: 50 )
const REQUESTS_COUNT = 50;
// Number of unique client IP for unique combination of subscriberID and ContentName ( Default : 4 )
const CLIENT_IPS = 4;
// Number of unique ContentName for unique combination of subscriberID ( Default : 4 )
const CONTENT_NAMES = 4;
// Number of unique SessionID for unique combination of subscriberID and clientIP ( Default : 1 )
const SESSION_IDS = 1;
/**
* Helper function to create an error with a defined status code
*
* @param {string} message - Error message
* @param {number} statusCode - HTTP status code
* @returns {Error} Error object with status code
*/
function createError(message, statusCode) {
const error = new Error(message);
error.statusCode = statusCode;
return error;
}
export class subscriberlog extends Resource {
/**
* Logs a new subscriber event and performs piracy checks
* POST /subscriberlog
*
* @param {Object} target - Request target
* @param {Promise<Object>} data - The subscriber event data (Promise)
* @param {Object} context - Request context
* @param {string} data.subscriberId - Subscriber ID (required)
* @param {string} data.clientsessionId - Client Session ID
* @param {string} data.clientIP - Client IP address
* @param {string} data.edgeIP - Edge IP address
* @param {string} data.Contentname - Content Name
* @param {string} data.useragent - User Agent of the request
* @param {string} data.Host - Host component of the URL
* @param {string} data.Path - Request Path component of URL
* @param {string} data.clientLocation - Client Location
* @throws {Error} If subscriberId is missing
* @returns {string} Confirmation message
*/
static async post(target, data, context) {
// Harper's default create permission (super_user only) is applied by the base
// `Resource.post`, which is a `transactional(...)` wrapper that calls
// `resource.allowCreate(context.user, ...)` before running the action. Defining a
// static `post` here shadows that wrapper, so REST dispatches to this method
// directly and the default gate never runs. Without this check, anonymous callers
// could write to `ratelimit.subscriber_log` — poisoning the piracy detector (push a
// subscriberId past the thresholds to deny service to a legitimate subscriber) and
// giving unauthenticated clients unbounded writes.
if (!context?.user?.role?.permission?.super_user) {
throw createError('Deny. Not authorized.', 403);
}
data = await data;
if (!data.subscriberId) {
throw createError('Deny. SubscriberId is required.', 400);
}
try {
const now = Date.now();
const startTime = now - (TIME_INTERVAL * 1000); // transform from seconds to milliseconds
// Building a new object to be stored in the SubscriberLog table.
const subLog = {
// The primary key `subscriberId` is stored as an array of subscriberId & time to make time based searches
// more performant and to allow multiple entries per subscriber.
subscriberId: [data.subscriberId, now],
clientsessionId: data.clientsessionId,
clientIP: data.clientIP,
edgeIP: data.edgeIP,
time: now,
contentname: data.Contentname,
useragent: data.useragent,
host: data.Host,
path: data.Path,
clientlocation: data.clientLocation,
}
// Run database put operation and piracy check concurrently
const [pirateConditions,] = await Promise.all([
// Piracy checks not taking in consideration current entry
subscriberlog.checkPirateConditions(data.subscriberId, startTime, now-1),
// Write subscriber log into DB
subscriber_log.put(subLog)
]);
// Set response headers based on piracy check results
context.responseHeaders.set('X-subscriber-pirate', pirateConditions.isPirate ? 'True' : 'False');
if (pirateConditions.isPirate) {
context.responseHeaders.set('X-subscriber-condition', pirateConditions.conditionHeader);
}
context.responseHeaders.set('X-subscriber-blacklist', 'False');
return "{'OK'}";
} catch (error) {
context.responseHeaders.set('X-Data-Update', String(error));
throw createError(String(error), 504);
}
}
/**
* Check for piracy conditions based on recent subscriber activity
* Performs a single database query and processes the data in memory
*
* @param {string} subscriberId - Subscriber ID
* @param {number} startTime - Start time for the check window in milliseconds
* @param {number} endTime - End time for the check window in milliseconds
* @returns {Object} Object indicating if the subscriber is a pirate and which condition was met
*/
static async checkPirateConditions(subscriberId, startTime, endTime) {
// Initialize data structures for condition checking
const requestsCount = new Map();
const uniqueClientIPs = new Map();
const uniqueContentNames = new Set();
const uniqueSessionIds = new Map();
const metConditions = new Set();
// Single database query to fetch all relevant logs
for await (const log of subscriber_log.search({
conditions: [
{ attribute: 'subscriberId', comparator: 'between', value: [[subscriberId, startTime], [subscriberId, Number(endTime)]] }
]
})) {
const contentName = log.contentname;
const clientIP = log.clientIP;
const sessionId = log.clientsessionId;
// Condition 1: Number of requests for unique combination of subscriberID and Contentname (in 10 seconds)
const requestKey = `${contentName}`;
requestsCount.set(requestKey, (requestsCount.get(requestKey) || 0) + 1);
if (requestsCount.get(requestKey) > REQUESTS_COUNT) {
metConditions.add('high_requests');
}
// Condition 2: Number of unique client IP for unique combination of subscriberID and ContentName
if (!uniqueClientIPs.has(contentName)) {
uniqueClientIPs.set(contentName, new Set());
}
uniqueClientIPs.get(contentName).add(clientIP);
if (uniqueClientIPs.get(contentName).size > CLIENT_IPS) {
metConditions.add('high_ip_count');
}
// Condition 3: Number of unique ContentName for unique combination of subscriberID
uniqueContentNames.add(contentName);
if (uniqueContentNames.size > CONTENT_NAMES) {
metConditions.add('multiple_content_views');
}
// Condition 4: Number of unique SessionID for unique combination of subscriberID and clientIP
const sessionKey = `${clientIP}`;
if (!uniqueSessionIds.has(sessionKey)) {
uniqueSessionIds.set(sessionKey, new Set());
}
uniqueSessionIds.get(sessionKey).add(sessionId);
if (uniqueSessionIds.get(sessionKey).size > SESSION_IDS) {
metConditions.add('multiple_sessions');
}
// Stop processing if all conditions are met
if (metConditions.size === 4) {
break;
}
}
// Determine if the subscriber is a pirate and which conditions were met
const isPirate = metConditions.size > 0;
const conditionHeader = Array.from(metConditions).join(',');
// Return piracy condition check result
return { isPirate: isPirate, conditionHeader: isPirate ? conditionHeader : undefined };
}
}