1 | package com.bowman.cardserv;
|
---|
2 |
|
---|
3 | import com.bowman.cardserv.interfaces.*;
|
---|
4 | import com.bowman.cardserv.tv.TvService;
|
---|
5 | import com.bowman.cardserv.util.*;
|
---|
6 | import com.bowman.cardserv.crypto.DESUtil;
|
---|
7 | import com.bowman.cardserv.cws.AbstractCwsConnector;
|
---|
8 |
|
---|
9 | import java.io.*;
|
---|
10 | import java.util.*;
|
---|
11 |
|
---|
12 | /**
|
---|
13 | * Created by IntelliJ IDEA.
|
---|
14 | * User: bowman
|
---|
15 | * Date: Apr 10, 2009
|
---|
16 | * Time: 00:45:17 AM
|
---|
17 | */
|
---|
18 | public class DcwFilterPlugin implements ProxyPlugin, ReplyFilter {
|
---|
19 |
|
---|
20 | private static final byte[] badDcw1 = DESUtil.stringToBytes("00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00");
|
---|
21 | private static final byte[] badDcw2 = DESUtil.stringToBytes("00 00 00 00 00 00 3C 3C 00 00 00 00 00 00 3C 3C");
|
---|
22 |
|
---|
23 | private ProxyLogger logger;
|
---|
24 | private Set badDcws = new HashSet();
|
---|
25 |
|
---|
26 | // these MessageCacheMaps will delete the oldest entry on every insertion, if it is older than 20 secs
|
---|
27 | private Map replyMap = Collections.synchronizedMap(new MessageCacheMap(20000));
|
---|
28 | private Map connMap = Collections.synchronizedMap(new MessageCacheMap(20000)); // dcw -> connector that recevied it
|
---|
29 | private Map sidLinksMap = Collections.synchronizedMap(new HashMap());
|
---|
30 | private Set removedLinks = new HashSet(), profiles;
|
---|
31 | private Map monitoredSidsMap = Collections.synchronizedMap(new MessageCacheMap(20000));
|
---|
32 | private Map invalidLinksMap = Collections.synchronizedMap(new HashMap());
|
---|
33 |
|
---|
34 | private Map zeroedReplyMap = Collections.synchronizedMap(new MessageCacheMap(20000));
|
---|
35 |
|
---|
36 | private boolean detectLinks = true, verifyReplies = false, forceContinuity = false;
|
---|
37 | private int verifiedCount = 0, badLengthCount = 0, filteredCount = 0, checksumFailCount = 0, mergeCount = 0;
|
---|
38 | private String badDcwStr;
|
---|
39 | private File mapFile;
|
---|
40 |
|
---|
41 | public DcwFilterPlugin() {
|
---|
42 | logger = ProxyLogger.getLabeledLogger(getClass().getName());
|
---|
43 | mapFile = new File("etc", "links.dat");
|
---|
44 | }
|
---|
45 |
|
---|
46 | public void configUpdated(ProxyXmlConfig xml) throws ConfigException {
|
---|
47 | Iterator iter = xml.getMultipleStrings("bad-dcw");
|
---|
48 | byte[] bd;
|
---|
49 | if(iter != null) while(iter.hasNext()) {
|
---|
50 | bd = DESUtil.stringToBytes((String)iter.next());
|
---|
51 | if(bd.length != 16)
|
---|
52 | throw new ConfigException(xml.getFullName(), "bad-dcw not 16 bytes: " + DESUtil.bytesToString(bd));
|
---|
53 | else badDcws.add(bd);
|
---|
54 | }
|
---|
55 | badDcws.add(badDcw1);
|
---|
56 | badDcws.add(badDcw2);
|
---|
57 |
|
---|
58 | detectLinks = "true".equalsIgnoreCase(xml.getStringValue("detect-links", "false"));
|
---|
59 | verifyReplies = "true".equalsIgnoreCase(xml.getStringValue("verify-replies", "false"));
|
---|
60 | forceContinuity = "true".equalsIgnoreCase(xml.getStringValue("force-continuity", "false"));
|
---|
61 |
|
---|
62 | String profilesStr = xml.getStringValue("profiles", "");
|
---|
63 | if(profilesStr != null && profilesStr.length() > 0) {
|
---|
64 | profiles = new HashSet(Arrays.asList(profilesStr.toLowerCase().split(" ")));
|
---|
65 | } else profiles = Collections.EMPTY_SET;
|
---|
66 | }
|
---|
67 |
|
---|
68 | public void start(CardServProxy proxy) {
|
---|
69 | Set set = new HashSet();
|
---|
70 | for(Iterator iter = badDcws.iterator(); iter.hasNext();) {
|
---|
71 | set.add(DESUtil.bytesToString((byte[])iter.next()));
|
---|
72 | }
|
---|
73 | badDcwStr = set.toString();
|
---|
74 | logger.info("Filtering bad CWs: " + badDcwStr);
|
---|
75 | loadLinksMap();
|
---|
76 | }
|
---|
77 |
|
---|
78 | public void stop() {
|
---|
79 | saveLinksMap();
|
---|
80 | }
|
---|
81 |
|
---|
82 | public String getName() {
|
---|
83 | return "DcwFilterPlugin";
|
---|
84 | }
|
---|
85 |
|
---|
86 | public String getDescription() {
|
---|
87 | return "Change bad dcws into empty newcamd replies (cannot decode) and do some basic analysis.";
|
---|
88 | }
|
---|
89 |
|
---|
90 | public Properties getProperties() {
|
---|
91 | Properties p = new Properties();
|
---|
92 | if(detectLinks) {
|
---|
93 | Set links = new TreeSet();
|
---|
94 | Set grp, strGrp;
|
---|
95 | String s, m;
|
---|
96 | String[] sa;
|
---|
97 | ProxyConfig config = ProxyConfig.getInstance();
|
---|
98 | long now = System.currentTimeMillis();
|
---|
99 | TvService srv;
|
---|
100 | for(Iterator iter = sidLinksMap.values().iterator(); iter.hasNext();) {
|
---|
101 | grp = (Set)iter.next();
|
---|
102 | strGrp = new TreeSet();
|
---|
103 | for(Iterator i = grp.iterator(); i.hasNext();) {
|
---|
104 | s = (String)i.next();
|
---|
105 | sa = s.split(":");
|
---|
106 | m = (monitoredSidsMap.containsKey(s) && (now - ((CamdNetMessage)monitoredSidsMap.get(s)).getTimeStamp() < 20000)) ? "*" : "";
|
---|
107 | if(profiles.isEmpty() || profiles.contains(sa[1].toLowerCase())) {
|
---|
108 | srv = config.getService(sa[1], Integer.parseInt(sa[0], 16));
|
---|
109 | if(srv.isTv()) strGrp.add(srv + m);
|
---|
110 | }
|
---|
111 | }
|
---|
112 | if(strGrp.size() >= 2) links.add(strGrp.toString());
|
---|
113 | }
|
---|
114 | p.setProperty("detected-service-links", links.toString());
|
---|
115 | p.setProperty("removed-service-links", removedLinks.toString());
|
---|
116 |
|
---|
117 | /*
|
---|
118 | TvService ts; Set monitoredStr = new TreeSet();
|
---|
119 | for(Iterator iter = monitoredSidsMap.keySet().iterator(); iter.hasNext(); ) {
|
---|
120 | s = (String)iter.next();
|
---|
121 | sa = s.split(":");
|
---|
122 | ts = config.getService(sa[1], Integer.parseInt(sa[0], 16));
|
---|
123 | if((now - ((CamdNetMessage)(monitoredSidsMap.get(s))).getTimeStamp()) < 20000)
|
---|
124 | monitoredStr.add(ts.toString() + "=" + (now - ((CamdNetMessage)(monitoredSidsMap.get(s))).getTimeStamp()));
|
---|
125 | }
|
---|
126 | p.setProperty("monitored-service-links", monitoredStr.toString());
|
---|
127 | p.setProperty("service-link-keys", sidLinksMap.keySet().toString());
|
---|
128 | */
|
---|
129 | }
|
---|
130 | if(verifyReplies) {
|
---|
131 | p.setProperty("verified-count", String.valueOf(verifiedCount));
|
---|
132 | if(!connMap.isEmpty()) p.setProperty("connector-map", String.valueOf(connMap.size()));
|
---|
133 | }
|
---|
134 | if(!replyMap.isEmpty()) p.setProperty("reply-map", String.valueOf(replyMap.size()));
|
---|
135 |
|
---|
136 | if(badDcwStr != null) p.setProperty("filtering-dcws", badDcwStr);
|
---|
137 |
|
---|
138 | if(filteredCount > 0) p.setProperty("filtered-count", String.valueOf(filteredCount));
|
---|
139 | if(badLengthCount > 0) p.setProperty("bad-length-count", String.valueOf(badLengthCount));
|
---|
140 | if(checksumFailCount > 0) p.setProperty("checksum-fail-count", String.valueOf(checksumFailCount));
|
---|
141 | if(mergeCount > 0) p.setProperty("merged-reply-count", String.valueOf(mergeCount));
|
---|
142 |
|
---|
143 | if(p.isEmpty()) return null;
|
---|
144 | else return p;
|
---|
145 | }
|
---|
146 |
|
---|
147 | protected void loadLinksMap() {
|
---|
148 | if(mapFile.exists()) {
|
---|
149 | try {
|
---|
150 | ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(new FileInputStream(mapFile)));
|
---|
151 | sidLinksMap = (Map)ois.readObject();
|
---|
152 | logger.fine("Loaded links map, " + sidLinksMap.size() + " entries.");
|
---|
153 | ois.close();
|
---|
154 | } catch(Exception e) {
|
---|
155 | logger.throwing(e);
|
---|
156 | logger.warning("Failed to load links map ('" + mapFile.getPath() + "'): " + e);
|
---|
157 | }
|
---|
158 | }
|
---|
159 | }
|
---|
160 |
|
---|
161 | protected void saveLinksMap() {
|
---|
162 | try {
|
---|
163 | ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(mapFile)));
|
---|
164 | oos.writeObject(sidLinksMap);
|
---|
165 | logger.fine("Saved links map, " + sidLinksMap.size() + " entries.");
|
---|
166 | oos.close();
|
---|
167 | } catch(IOException e) {
|
---|
168 | logger.throwing(e);
|
---|
169 | logger.warning("Failed to save links map ('" + mapFile.getPath() + "'): " + e);
|
---|
170 | }
|
---|
171 | }
|
---|
172 |
|
---|
173 | public CamdNetMessage doFilter(ProxySession session, CamdNetMessage msg) {
|
---|
174 | return msg; // do nothing with ecm requests from clients
|
---|
175 | }
|
---|
176 |
|
---|
177 | public CamdNetMessage doReplyFilter(CwsConnector connector, CamdNetMessage msg) {
|
---|
178 | try {
|
---|
179 | if(msg.isDcw() && msg.getDataLength() != 0) { // ignore other types of replies and cannot-decodes
|
---|
180 |
|
---|
181 | if(profiles.isEmpty() || profiles.contains(msg.getProfileName())) {
|
---|
182 |
|
---|
183 | if(msg.getDataLength() != 16) { // check for bad length
|
---|
184 | logger.warning("Bad DCW length (" + msg.getDataLength() + ") from '" + connector.getName() + "': " +
|
---|
185 | DESUtil.bytesToString(msg.getCustomData()));
|
---|
186 | msg.setCustomData(new byte[0]); // turn bad length dcws into cannot-decodes
|
---|
187 | badLengthCount++;
|
---|
188 |
|
---|
189 | } else if(!checksumDcw(msg.getCustomData())) { // verify dcw checksums
|
---|
190 | logger.warning("Bad DCW checksum in reply from '" + connector.getName() + "': " +
|
---|
191 | DESUtil.bytesToString(msg.getCustomData()));
|
---|
192 | msg.setCustomData(new byte[0]); // turn bad checksum dcws into cannot-decodes
|
---|
193 | checksumFailCount++;
|
---|
194 |
|
---|
195 | } else if(isBadDcw(msg, badDcws)) { // verify against list of preconfigured bad replies
|
---|
196 | logger.warning("Bad DCW reply from '" + connector.getName() + "': " +
|
---|
197 | DESUtil.bytesToString(msg.getCustomData()));
|
---|
198 | msg.setCustomData(new byte[0]); // turn preconfigured bad dcws into cannot-decodes
|
---|
199 | filteredCount++;
|
---|
200 |
|
---|
201 | } else {
|
---|
202 |
|
---|
203 | // check for instances of the same dcw reply being used for other currently watched services
|
---|
204 | if(detectLinks) detectLink(msg);
|
---|
205 |
|
---|
206 | // check for zeroed out dcws and attempt to reinsert the previous one (if availble) to help certain clients
|
---|
207 | if(forceContinuity) forceContinuity(msg);
|
---|
208 |
|
---|
209 | boolean blockReply = false;
|
---|
210 |
|
---|
211 | if(verifyReplies) {
|
---|
212 | // require each reply to be returned from 2 different connectors
|
---|
213 | // if it isn't, block it here and only release if the same reply is received from another connector
|
---|
214 | blockReply = !verifyReply(msg, connector);
|
---|
215 | }
|
---|
216 |
|
---|
217 | if(detectLinks || verifyReplies) replyMap.put(msg, msg); // keep a 20 second backlog of all received dcws
|
---|
218 |
|
---|
219 | if(blockReply) return null;
|
---|
220 |
|
---|
221 | }
|
---|
222 | }
|
---|
223 | }
|
---|
224 | } catch(Throwable t) {
|
---|
225 | t.printStackTrace(); // intentional catch-all while debugging this use case
|
---|
226 | }
|
---|
227 | return msg;
|
---|
228 | }
|
---|
229 |
|
---|
230 | private static boolean hasZeroDcw(byte[] data) {
|
---|
231 | return data[0] + data[1] + data[3] == 0 || data[9] + data[10] + data[11] == 0;
|
---|
232 | }
|
---|
233 |
|
---|
234 | private static void mergeZeroedReplies(CamdNetMessage currMsg, CamdNetMessage prevMsg) {
|
---|
235 | byte[] curr = currMsg.getCustomData(), prev = prevMsg.getCustomData();
|
---|
236 | for(int i = 0; i < curr.length; i++) curr[i] |= prev[i];
|
---|
237 | }
|
---|
238 |
|
---|
239 | private static boolean checksumDcw(byte[] data) {
|
---|
240 | if(data[3] != (byte)((data[0] + data[1] + data[2]) & 0xFF) || data[7] != (byte)((data[4] + data[5] + data[6]) & 0xFF)) {
|
---|
241 | return false;
|
---|
242 | }
|
---|
243 | if(data[11] != (byte)((data[8] + data[9] + data[10]) & 0xFF) || data[15] != (byte)((data[12] + data[13] + data[14]) & 0xFF)) {
|
---|
244 | return false;
|
---|
245 | }
|
---|
246 | return true;
|
---|
247 | }
|
---|
248 |
|
---|
249 | private static boolean isBadDcw(CamdNetMessage msg, Set badDcws) {
|
---|
250 | byte[] badDcw;
|
---|
251 | for(Iterator iter = badDcws.iterator(); iter.hasNext();) {
|
---|
252 | badDcw = (byte[])iter.next();
|
---|
253 | if(Arrays.equals(msg.getCustomData(), badDcw)) {
|
---|
254 | return true;
|
---|
255 | }
|
---|
256 | }
|
---|
257 | return false;
|
---|
258 | }
|
---|
259 |
|
---|
260 | private boolean forceContinuity(CamdNetMessage msg) {
|
---|
261 | if(msg.getServiceId() != 0) {
|
---|
262 | // System.out.println("Checking for 00 dcw...");
|
---|
263 | if(hasZeroDcw(msg.getCustomData())) {
|
---|
264 | String context = msg.getServiceId() + ":" + msg.getProfileName();
|
---|
265 | String key = (msg.getCommandTag() == 0x81?"81":"80") + ":" + context;
|
---|
266 | // System.out.println("00 dcw received: \t" + key + " - " + DESUtil.bytesToString(msg.getCustomData()));
|
---|
267 | zeroedReplyMap.put(key, new CamdNetMessage(msg));
|
---|
268 | String prevKey = (msg.getCommandTag() == 0x81?"80":"81") + ":" + context;
|
---|
269 | CamdNetMessage prev = (CamdNetMessage)zeroedReplyMap.get(prevKey);
|
---|
270 | if(prev != null) {
|
---|
271 | long age = System.currentTimeMillis() - prev.getTimeStamp();
|
---|
272 | if(age < 12000) {
|
---|
273 | // System.out.println("Prev dcw found: \t" + prevKey + " - " + DESUtil.bytesToString(prev.getCustomData()) + " (age: " + age);
|
---|
274 | mergeZeroedReplies(msg, prev);
|
---|
275 | mergeCount++;
|
---|
276 | // System.out.println("Resulting reply: \t" + key + " - " + DESUtil.bytesToString(msg.getCustomData()));
|
---|
277 | return true;
|
---|
278 | }
|
---|
279 | }
|
---|
280 | }
|
---|
281 | }
|
---|
282 | return false;
|
---|
283 | }
|
---|
284 |
|
---|
285 | private boolean detectLink(CamdNetMessage msg) {
|
---|
286 | if(msg.getServiceId() != 0) { // dcw has sid, check for services sharing the same dcw sequence
|
---|
287 | String msgId = Integer.toHexString(msg.getServiceId()) + ":" + msg.getProfileName();
|
---|
288 | if(sidLinksMap.containsKey(msgId)) {
|
---|
289 | monitoredSidsMap.put(msgId, msg);
|
---|
290 | // check if previously detected links can be disproven with current traffic
|
---|
291 | String testId;
|
---|
292 | for(Iterator iter = (new ArrayList((Set)sidLinksMap.get(msgId))).iterator(); iter.hasNext();) {
|
---|
293 | testId = (String)iter.next();
|
---|
294 | if(testId.equals(msgId)) continue;
|
---|
295 | if(monitoredSidsMap.containsKey(testId)) {
|
---|
296 | CamdNetMessage prev = (CamdNetMessage)monitoredSidsMap.get(testId);
|
---|
297 | long now = System.currentTimeMillis();
|
---|
298 | if(now - prev.getTimeStamp() < 2000) { // zapping might cause false positives here?
|
---|
299 | if(!prev.equals(msg)) {
|
---|
300 | ProxyConfig config = ProxyConfig.getInstance();
|
---|
301 | String[] sa = testId.split(":");
|
---|
302 | TvService m = config.getService(msg.getProfileName(), msg.getServiceId());
|
---|
303 | TvService t = config.getService(sa[1], Integer.parseInt(sa[0], 16));
|
---|
304 | Set l = new TreeSet();
|
---|
305 | l.add(m.toString());
|
---|
306 | l.add(t.toString());
|
---|
307 | String link = l.toString();
|
---|
308 | Long timeStamp = (Long)invalidLinksMap.get(link);
|
---|
309 | if(timeStamp != null && (now - timeStamp.longValue() < 20000)) { // two mismatches in a row in under 20 sec = probable invalid link
|
---|
310 | logger.info("Link no longer seems valid, removing: " + m + " > " + t);
|
---|
311 | removedLinks.add(link);
|
---|
312 | Set s = (Set)sidLinksMap.get(msgId);
|
---|
313 | s.remove(testId);
|
---|
314 | if(s.size() <= 1) {
|
---|
315 | sidLinksMap.remove(msgId);
|
---|
316 | sidLinksMap.remove(testId);
|
---|
317 | }
|
---|
318 | } else invalidLinksMap.put(link, new Long(now));
|
---|
319 | }
|
---|
320 | }
|
---|
321 | }
|
---|
322 | }
|
---|
323 | }
|
---|
324 |
|
---|
325 | if(replyMap.containsKey(msg)) {
|
---|
326 | CamdNetMessage prev = (CamdNetMessage)replyMap.get(msg);
|
---|
327 | if(prev.getServiceId() != msg.getServiceId()) {
|
---|
328 | String prevId = Integer.toHexString(prev.getServiceId()) + ":" + prev.getProfileName();
|
---|
329 | Set links = (Set)sidLinksMap.get(prevId);
|
---|
330 | if(links == null) links = (Set)sidLinksMap.get(msgId);
|
---|
331 | if(links == null) links = new TreeSet();
|
---|
332 | links.add(prevId);
|
---|
333 | links.add(msgId);
|
---|
334 | if(sidLinksMap.put(prevId, links) == null || sidLinksMap.put(msgId, links) == null) {
|
---|
335 | logger.fine("Link found: " + links + "\n\t" + prev + "\n\t" + msg);
|
---|
336 | return true;
|
---|
337 | }
|
---|
338 | }
|
---|
339 | }
|
---|
340 | }
|
---|
341 | return false;
|
---|
342 | }
|
---|
343 |
|
---|
344 | private boolean verifyReply(CamdNetMessage msg, CwsConnector connector) {
|
---|
345 | CwsConnector prevConn = (CwsConnector)connMap.put(msg, connector);
|
---|
346 | if(prevConn != null) {
|
---|
347 | if(prevConn != connector) { // same reply has previously been received from another connector
|
---|
348 | verifiedCount++;
|
---|
349 | ((AbstractCwsConnector)prevConn).reportReply((CamdNetMessage)replyMap.get(msg)); // retrieve and re-introduce
|
---|
350 | return true;
|
---|
351 | }
|
---|
352 | }
|
---|
353 | // no previous encounter with this reply
|
---|
354 | return false;
|
---|
355 | }
|
---|
356 |
|
---|
357 | public byte[] getResource(String path, boolean admin) {
|
---|
358 | return null;
|
---|
359 | }
|
---|
360 |
|
---|
361 | public byte[] getResource(String path, byte[] inData, boolean admin) {
|
---|
362 | return null;
|
---|
363 | }
|
---|
364 |
|
---|
365 | }
|
---|