protected boolean handleLocalTunnel( TrackerWebPageRequest request, TrackerWebPageResponse response) throws IOException { start(); if (SRP_VERIFIER == null || !active) { throw (new IOException("Secure pairing is not enabled")); } boolean good_request = false; try { // remove /pairing/tunnel/ String url = request.getURL().substring(16); int q_pos = url.indexOf('?'); Map<String, String> args = new HashMap<String, String>(); if (q_pos != -1) { String args_str = url.substring(q_pos + 1); String[] bits = args_str.split("&"); for (String arg : bits) { String[] x = arg.split("="); if (x.length == 2) { args.put(x[0].toLowerCase(), x[1]); } } url = url.substring(0, q_pos); } if (url.startsWith("create")) { String ac = args.get("ac"); String sid = args.get("sid"); if (ac == null || sid == null) { throw (new IOException("Access code or service id missing")); } if (!ac.equals(manager.peekAccessCode())) { throw (new IOException("Invalid access code")); } PairedServiceImpl ps = manager.getService(sid); if (ps == null) { good_request = true; throw (new IOException("Service '" + sid + "' not registered")); } PairedServiceRequestHandler handler = ps.getHandler(); if (handler == null) { good_request = true; throw (new IOException("Service '" + sid + "' has no handler registered")); } JSONObject json = new JSONObject(); JSONObject result = new JSONObject(); json.put("result", result); byte[] ss = new byte[] {SRP_SALT[0], SRP_SALT[1], SRP_SALT[2], SRP_SALT[3]}; long tunnel_id = RandomUtils.nextSecureAbsoluteLong(); String tunnel_name = Base32.encode(ss) + "_" + tunnel_id; synchronized (local_server_map) { long diff = SystemTime.getMonotonousTime() - last_local_server_create_time; if (diff < 5000) { try { long sleep = 5000 - diff; System.out.println("Sleeping for " + sleep + " before starting srp"); Thread.sleep(sleep); } catch (Throwable e) { } } SRP6Server server = new SRP6Server(); server.init(N_3072, G_3072, SRP_VERIFIER, new SHA256Digest(), RandomUtils.SECURE_RANDOM); BigInteger B = server.generateServerCredentials(); local_server_map.put(tunnel_name, new Object[] {server, handler, null, null}); last_local_server_create_time = SystemTime.getMonotonousTime(); total_local_servers++; result.put("srp_salt", Base32.encode(SRP_SALT)); result.put("srp_b", Base32.encode(B.toByteArray())); Map<String, String> headers = request.getHeaders(); String host = headers.get("host"); // remove port number int pos = host.lastIndexOf("]"); if (pos != -1) { // ipv6 literal host = host.substring(0, pos + 1); } else { pos = host.indexOf(':'); if (pos != -1) { host = host.substring(0, pos); } } String abs_url = request.getAbsoluteURL().toString(); // unfortunately there is some nasty code that uses a configured tracker // address as the default host abs_url = UrlUtils.setHost(new URL(abs_url), host).toExternalForm(); pos = abs_url.indexOf("/create"); String tunnel_url = abs_url.substring(0, pos) + "/id/" + tunnel_name; result.put("url", tunnel_url); } response.getOutputStream().write(JSONUtils.encodeToJSON(json).getBytes("UTF-8")); response.setContentType("application/json; charset=UTF-8"); response.setGZIP(true); good_request = true; return (true); } else if (url.startsWith("id/")) { String tunnel_name = url.substring(3); Object[] entry; synchronized (local_server_map) { entry = local_server_map.get(tunnel_name); if (entry == null) { good_request = true; throw (new IOException("Unknown tunnel id")); } } String srp_a = args.get("srp_a"); String enc_data = args.get("enc_data"); String enc_iv = args.get("enc_iv"); if (srp_a != null && enc_data != null && enc_iv != null) { try { synchronized (local_server_map) { long diff = SystemTime.getMonotonousTime() - last_local_server_agree_time; if (diff < 5000) { try { long sleep = 5000 - diff; System.out.println("Sleeping for " + sleep + " before completing srp"); Thread.sleep(sleep); } catch (Throwable e) { } } } JSONObject json = new JSONObject(); JSONObject result = new JSONObject(); json.put("result", result); SRP6Server server = (SRP6Server) entry[0]; BigInteger A = new BigInteger(Base32.decode(srp_a)); BigInteger serverS = server.calculateSecret(A); byte[] shared_secret = serverS.toByteArray(); Cipher decipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); byte[] key = new byte[16]; System.arraycopy(shared_secret, 0, key, 0, 16); SecretKeySpec secret = new SecretKeySpec(key, "AES"); decipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(Base32.decode(enc_iv))); byte[] dec = decipher.doFinal(Base32.decode(enc_data)); JSONObject dec_json = (JSONObject) JSONUtils.decodeJSON(new String(dec, "UTF-8")); String tunnel_url = (String) dec_json.get("url"); if (!tunnel_url.contains(tunnel_name)) { throw (new IOException("Invalid tunnel url")); } String endpoint_url = (String) dec_json.get("endpoint"); entry[2] = secret; entry[3] = endpoint_url; result.put("state", "activated"); response.getOutputStream().write(JSONUtils.encodeToJSON(json).getBytes("UTF-8")); response.setContentType("application/json; charset=UTF-8"); response.setGZIP(true); good_request = true; return (true); } catch (Throwable e) { throw (new IOException(Debug.getNestedExceptionMessage(e))); } finally { last_local_server_agree_time = SystemTime.getMonotonousTime(); } } else if (args.containsKey("close")) { synchronized (local_server_map) { local_server_map.remove(tunnel_name); } good_request = true; return (true); } else { PairedServiceRequestHandler request_handler = (PairedServiceRequestHandler) entry[1]; SecretKeySpec secret = (SecretKeySpec) entry[2]; String endpoint_url = (String) entry[3]; if (secret == null) { throw (new IOException("auth not completed")); } byte[] request_data = FileUtil.readInputStreamAsByteArray(request.getInputStream()); try { byte[] decrypted; { byte[] IV = new byte[16]; System.arraycopy(request_data, 0, IV, 0, IV.length); Cipher decipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); decipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(IV)); decrypted = decipher.doFinal(request_data, 16, request_data.length - 16); } byte[] reply_bytes = request_handler.handleRequest( request.getClientAddress2().getAddress(), endpoint_url, decrypted); { Cipher encipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); encipher.init(Cipher.ENCRYPT_MODE, secret); AlgorithmParameters params = encipher.getParameters(); byte[] IV = params.getParameterSpec(IvParameterSpec.class).getIV(); byte[] enc = encipher.doFinal(reply_bytes); byte[] rep_bytes = new byte[IV.length + enc.length]; System.arraycopy(IV, 0, rep_bytes, 0, IV.length); System.arraycopy(enc, 0, rep_bytes, IV.length, enc.length); response.getOutputStream().write(rep_bytes); response.setContentType("application/octet-stream"); good_request = true; return (true); } } catch (Throwable e) { throw (new IOException(Debug.getNestedExceptionMessage(e))); } } } throw (new IOException("Unknown tunnel operation")); } finally { if (!good_request) { manager.recordRequest( "SRP", request.getClientAddress2().getAddress().getHostAddress(), false); } } }
protected boolean generate(TrackerWebPageRequest request, TrackerWebPageResponse response) throws IOException { InetSocketAddress local_address = request.getLocalAddress(); if (local_address == null) { return (false); } String host = local_address.getAddress().getHostAddress(); String url = request.getURL(); if (TRACE) { System.out.println("url: " + url); } if (!url.startsWith("/TiVoConnect?")) { return (false); } int pos = url.indexOf('?'); if (pos == -1) { return (false); } String[] bits = url.substring(pos + 1).split("&"); Map<String, String> args = new HashMap<String, String>(); for (String bit : bits) { String[] x = bit.split("="); args.put(x[0], URLDecoder.decode(x[1], "UTF-8")); } if (TRACE) { System.out.println("args: " + args); } // root folder /TiVoConnect?Command=QueryContainer&Container=%2F String command = args.get("Command"); if (command == null) { return (false); } String reply = null; if (command.equals("QueryContainer")) { String container = args.get("Container"); if (container == null) { return (false); } if (container.equals("/")) { reply = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>" + NL + "<TiVoContainer>" + NL + " <Details>" + NL + " <Title>" + server_name + "</Title>" + NL + " <ContentType>x-container/tivo-server</ContentType>" + NL + " <SourceFormat>x-container/folder</SourceFormat>" + NL + " <TotalItems>1</TotalItems>" + NL + " </Details>" + NL + " <Item>" + NL + " <Details>" + NL + " <Title>" + server_name + "</Title>" + NL + " <ContentType>x-container/tivo-videos</ContentType>" + NL + " <SourceFormat>x-container/folder</SourceFormat>" + NL + " </Details>" + NL + " <Links>" + NL + " <Content>" + NL + " <Url>/TiVoConnect?Command=QueryContainer&Container=" + urlencode("/Content") + "</Url>" + NL + " <ContentType>x-container/tivo-videos</ContentType>" + NL + " </Content>" + NL + " </Links>" + NL + " </Item>" + NL + " <ItemStart>0</ItemStart>" + NL + " <ItemCount>1</ItemCount>" + NL + "</TiVoContainer>"; } else if (container.startsWith("/Content")) { boolean show_categories = getShowCategories(); String recurse = args.get("Recurse"); if (recurse != null && recurse.equals("Yes")) { show_categories = false; } TranscodeFileImpl[] tfs = getFiles(); String category_or_tag = null; Map<String, ContainerInfo> categories_or_tags = null; if (show_categories) { if (container.startsWith("/Content/")) { category_or_tag = container.substring(container.lastIndexOf('/') + 1); } else { categories_or_tags = new HashMap<String, ContainerInfo>(); } } // build list of applicable items List<ItemInfo> items = new ArrayList<ItemInfo>(tfs.length); for (TranscodeFileImpl file : tfs) { if (!file.isComplete()) { // see if we can set up a stream xcode for this but only if we // know the duration and the transcode is in progress (done in setup) if (!setupStreamXCode(file)) { continue; } } if (category_or_tag != null) { boolean hit = false; String[] cats = file.getCategories(); String[] tags = file.getTags(true); for (String[] strs : new String[][] {cats, tags}) { for (String c : strs) { if (c.equals(category_or_tag)) { hit = true; } } } if (!hit) { continue; } } FileInfo info = new FileInfo(file, host); if (info.isOK()) { boolean skip = false; if (categories_or_tags != null) { String[] cats = file.getCategories(); String[] tags = file.getTags(true); if (cats.length > 0 || tags.length > 0) { skip = true; for (String[] strs : new String[][] {cats, tags}) { for (String s : strs) { ContainerInfo cont = categories_or_tags.get(s); if (cont == null) { items.add(cont = new ContainerInfo(s)); categories_or_tags.put(s, cont); } cont.addChild(); } } } } if (!skip) { items.add(info); } } } // sort String sort_order = args.get("SortOrder"); if (sort_order != null) { String[] keys = Constants.PAT_SPLIT_COMMA.split(sort_order); final List<Comparator<ItemInfo>> comparators = new ArrayList<Comparator<ItemInfo>>(); final List<Boolean> reverses = new ArrayList<Boolean>(); for (String key : keys) { boolean reverse = false; if (key.startsWith("!")) { reverse = true; key = key.substring(1); } Comparator<ItemInfo> comp = sort_comparators.get(key); if (comp != null) { comparators.add(comp); reverses.add(reverse); } } if (comparators.size() > 0) { Collections.sort( items, new Comparator<ItemInfo>() { public int compare(ItemInfo i1, ItemInfo i2) { for (int i = 0; i < comparators.size(); i++) { Comparator<ItemInfo> comp = comparators.get(i); int res = comp.compare(i1, i2); if (res != 0) { if (reverses.get(i)) { if (res < 0) { res = 1; } else { res = -1; } } return (res); } } return (0); } }); } } // select items to return String item_count = args.get("ItemCount"); String anchor_offset = args.get("AnchorOffset"); String anchor = args.get("AnchorItem"); int num_items; if (item_count == null) { num_items = items.size(); } else { // can be negative if X items from end num_items = Integer.parseInt(item_count); } int anchor_index; // either one before or one after item to be returned depending on count // +ve/-ve if (num_items < 0) { anchor_index = items.size(); } else { anchor_index = -1; } if (anchor != null) { for (int i = 0; i < items.size(); i++) { ItemInfo info = items.get(i); if (anchor.equals(info.getLinkURL())) { anchor_index = i; } } } if (anchor_offset != null) { anchor_index += Integer.parseInt(anchor_offset); if (anchor_index < -1) { anchor_index = -1; } else if (anchor_index > items.size()) { anchor_index = items.size(); } } int start_index; int end_index; if (num_items > 0) { start_index = anchor_index + 1; end_index = anchor_index + num_items; } else { start_index = anchor_index + num_items; end_index = anchor_index - 1; } if (start_index < 0) { start_index = 0; } if (end_index >= items.size()) { end_index = items.size() - 1; } int num_to_return = end_index - start_index + 1; if (num_to_return < 0) { num_to_return = 0; } String machine = getMachineName(); if (machine == null) { // default until we find out what it is - can't see any way to get it apart from wait for // broadcast machine = "TivoHDDVR"; } String header = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>" + NL + "<TiVoContainer>" + NL + " <Tivos>" + NL + " <Tivo>" + machine + "</Tivo>" + NL + " </Tivos>" + NL + " <ItemStart>" + start_index + "</ItemStart>" + NL + " <ItemCount>" + num_to_return + "</ItemCount>" + NL + " <Details>" + NL + " <Title>" + escape(container) + "</Title>" + NL + " <ContentType>x-container/tivo-videos</ContentType>" + NL + " <SourceFormat>x-container/folder</SourceFormat>" + NL + " <TotalItems>" + items.size() + "</TotalItems>" + NL + " </Details>" + NL; reply = header; for (int i = start_index; i <= end_index; i++) { ItemInfo item = items.get(i); if (item instanceof FileInfo) { FileInfo file = (FileInfo) item; long file_size = file.getTargetSize(); String title = escape(file.getName()); String desc = title; int MAX_TITLE_LENGTH = 30; if (title.length() > MAX_TITLE_LENGTH) { // TiVo has problems displaying a truncated title if it has // no spaces in it String temp = ""; for (int j = 0; j < title.length(); j++) { char c = title.charAt(j); if (Character.isLetterOrDigit(c)) { temp += c; } else { temp += ' '; } } int space_pos = temp.indexOf(' '); if (space_pos == -1 || space_pos > MAX_TITLE_LENGTH) { temp = temp.substring(0, 30) + "..."; } title = temp; } reply += " <Item>" + NL + " <Details>" + NL + " <Title>" + title + "</Title>" + NL + " <ContentType>video/x-tivo-mpeg</ContentType>" + NL + " <SourceFormat>video/x-ms-wmv</SourceFormat>" + NL; if (file_size > 0) { reply += " <SourceSize>" + file_size + "</SourceSize>" + NL; } else { long est_size = file.getEstimatedTargetSize(); if (est_size > 0) { reply += " <SourceSize>" + est_size + "</SourceSize>" + NL; } } reply += " <Duration>" + file.getDurationMillis() + "</Duration>" + NL + " <Description>" + desc + "</Description>" + NL + " <SourceChannel>0</SourceChannel>" + NL + " <SourceStation></SourceStation>" + NL + " <SeriesId></SeriesId>" + NL + " <CaptureDate>" + file.getCaptureDate() + "</CaptureDate>" + NL + " </Details>" + NL + " <Links>" + NL + " <Content>" + NL + " <ContentType>video/x-tivo-mpeg</ContentType>" + NL + " <AcceptsParams>No</AcceptsParams>" + NL + " <Url>" + file.getLinkURL() + "</Url>" + NL + " </Content>" + NL + " <CustomIcon>" + NL + " <ContentType>video/*</ContentType>" + NL + " <AcceptsParams>No</AcceptsParams>" + NL + " <Url>urn:tivo:image:save-until-i-delete-recording</Url>" + NL + " </CustomIcon>" + NL + " </Links>" + NL + " </Item>" + NL; } else { ContainerInfo cont = (ContainerInfo) item; reply += " <Item>" + NL + " <Details>" + NL + " <Title>" + cont.getName() + "</Title>" + NL + " <ContentType>x-container/tivo-videos</ContentType>" + NL + " <SourceFormat>x-container/folder</SourceFormat>" + NL + " <TotalItems>" + cont.getChildCount() + "</TotalItems>" + NL + " </Details>" + NL + " <Links>" + NL + " <Content>" + NL + " <Url>" + cont.getLinkURL() + "</Url>" + NL + " <ContentType>x-container/tivo-videos</ContentType>" + NL + " </Content>" + NL + " </Links>" + NL + " </Item>" + NL; } } String footer = "</TiVoContainer>"; reply += footer; } } else if (command.equals("QueryFormats")) { String source_format = args.get("SourceFormat"); if (source_format != null && source_format.startsWith("video")) { // /TiVoConnect?Command=QueryFormats&SourceFormat=video%2Fx-tivo-mpeg reply = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" + NL + "<TiVoFormats><Format>" + NL + "<ContentType>video/x-tivo-mpeg</ContentType><Description/>" + NL + "</Format></TiVoFormats>"; } } if (reply == null) { return (false); } if (TRACE) { System.out.println("->" + reply); } response.setContentType("text/xml"); response.getOutputStream().write(reply.getBytes("UTF-8")); return (true); }