/******************************************************************************* * Copyright (c) 2013 Jens Kristian Villadsen. * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0 * which accompanies this distribution, and is available at * http://www.gnu.org/licenses/gpl.html * * Contributors: * Jens Kristian Villadsen - Lead developer, owner and creator ******************************************************************************/ package org.dyndns.jkiddo.service.dmap; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.URL; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Collections; import java.util.zip.GZIPInputStream; import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.ResponseBuilder; import org.dyndns.jkiddo.NotImplementedException; import org.dyndns.jkiddo.dmp.chunks.Chunk; import org.dyndns.jkiddo.dmp.util.DmapUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.dd.plist.NSDictionary; import com.dd.plist.NSNumber; import com.dd.plist.NSString; import com.dd.plist.PropertyListParser; import com.sun.jersey.core.spi.factory.ResponseBuilderImpl; public class Util { public static final String APPLICATION_NAME = "APPLICATION_NAME"; public static final String APPLICATION_X_DMAP_TAGGED = "application/x-dmap-tagged"; private static final Logger LOGGER = LoggerFactory.getLogger(Util.class); private static final int PARTIAL_CONTENT = 206; public static Response buildResponse(final Chunk chunk, final String dmapKey, final String dmapServiceName) throws IOException { final byte[] binaryChunk = DmapUtil.serialize(chunk, false); return buildResponse(dmapKey, dmapServiceName).entity(binaryChunk).header(HttpHeaders.CONTENT_LENGTH, String.valueOf(binaryChunk.length)).build();// .header("Content-Encoding", "gzip").build(); } public static Response buildAudioResponse(final byte[] buffer, final long position, final String dmapKey, final String dmapServiceName) { final ResponseBuilder response = new ResponseBuilderImpl().header("Accept-Ranges", "bytes").header(HttpHeaders.DATE, DmapUtil.now()).header(dmapKey, dmapServiceName).header(HttpHeaders.CONTENT_TYPE, APPLICATION_X_DMAP_TAGGED).header("Connection", "close"); if(position == 0) { response.status(Response.Status.OK); response.header(HttpHeaders.CONTENT_LENGTH, Long.toString(buffer.length)); } else { response.status(PARTIAL_CONTENT); response.header(HttpHeaders.CONTENT_LENGTH, Long.toString(buffer.length - position)); response.header("Content-Range", "bytes " + position + "-" + (buffer.length - 1) + "/" + buffer.length); } response.entity(buffer); return response.build(); } public static Response buildBinaryResponse(final byte[] buffer, final String dmapKey, final String dmapServiceName) { final ResponseBuilder response = new ResponseBuilderImpl().header(HttpHeaders.DATE, DmapUtil.now()).header(dmapKey, dmapServiceName).header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM).header(HttpHeaders.CONTENT_ENCODING, "gzip"); response.status(Response.Status.OK); response.entity(buffer); response.header(HttpHeaders.CONTENT_LENGTH, Long.toString(buffer.length)); return response.build(); } private static ResponseBuilder buildResponse(final String dmapKey, final String dmapServiceName) { return new ResponseBuilderImpl().header(HttpHeaders.DATE, DmapUtil.now()).header(dmapKey, dmapServiceName).header(HttpHeaders.CONTENT_TYPE, APPLICATION_X_DMAP_TAGGED).header("Connection", "Keep-Alive").status(Response.Status.OK); } enum SecurityType { BASIC, DIGEST } public static Response buildAuthenticationResponse(final String dmapKey, final String dmapServiceName, final SecurityType sm) throws NoSuchAlgorithmException, UnsupportedEncodingException { final ResponseBuilder builder = new ResponseBuilderImpl().header(HttpHeaders.DATE, DmapUtil.now()).header(dmapKey, dmapServiceName).header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_HTML).header(HttpHeaders.CONTENT_LENGTH, "0").header("Connection", "Keep-Alive").status(Response.Status.UNAUTHORIZED); switch(sm) { case BASIC: builder.header(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"" + DmapUtil.DAAP_REALM + "\""); break; case DIGEST: builder.header(HttpHeaders.WWW_AUTHENTICATE, "Digest realm=\"" + DmapUtil.DAAP_REALM + "\", nonce=\"" + DmapUtil.nonce() + "\""); break; default: throw new NotImplementedException(); } return builder.build(); } public static Response buildEmptyResponse(final String dmapKey, final String dmapServiceName) { return buildResponse(dmapKey, dmapServiceName).status(Response.Status.NO_CONTENT).build(); } public static String toHex(final String value) { try { return toHex(value.getBytes("UTF-8")); } catch(final UnsupportedEncodingException e) { throw new RuntimeException(e); } } public static String toHex(final byte[] code) { final StringBuilder sb = new StringBuilder(); for(final byte b : code) { sb.append(String.format("%02x", b & 0xff)); } return sb.toString().toUpperCase(); } public static String toServiceGuid(final String name) { try { return toHex((name + "1111111111111111").getBytes("UTF-8")).substring(0, 16); } catch(final UnsupportedEncodingException e) { throw new RuntimeException(e); } } public static String fromHex(final String hex) { final StringBuilder str = new StringBuilder(); for(int i = 0; i < hex.length(); i += 2) { str.append((char) Integer.parseInt(hex.substring(i, i + 2), 16)); } return str.toString(); } /** * Converts an array of bytes to a hexadecimal string * * @param bytes * array of bytes * @return hexadecimal representation */ public static String toHexString(final byte[] bytes) { final StringBuilder s = new StringBuilder(); for(final byte b : bytes) { final String h = Integer.toHexString(0x100 | b); s.append(h.substring(h.length() - 2, h.length()).toUpperCase()); } return s.toString(); } public static String toMacString(final byte[] bytes) { final String hex = toHexString(bytes); return hex.substring(0, 2) + ":" + hex.substring(2, 4) + ":" + hex.substring(4, 6) + ":" + hex.substring(6, 8) + ":" + hex.substring(8, 10) + ":" + hex.substring(10, 12); } /** * Returns a suitable hardware address. * * @return a MAC address */ public static byte[] getHardwareAddress() { try { /* Search network interfaces for an interface with a valid, non-blocked hardware address */ for(final NetworkInterface iface : Collections.list(NetworkInterface.getNetworkInterfaces())) { if(iface.isLoopback()) continue; if(iface.isPointToPoint()) continue; if(!iface.isUp()) continue; try { final byte[] ifaceMacAddress = iface.getHardwareAddress(); if((ifaceMacAddress != null) && (ifaceMacAddress.length == 6) && !isBlockedHardwareAddress(ifaceMacAddress)) { LOGGER.info("Hardware address is " + toHexString(ifaceMacAddress) + " (" + iface.getDisplayName() + ")"); return Arrays.copyOfRange(ifaceMacAddress, 0, 6); } } catch(final Throwable e) { /* Ignore */ } } } catch(final Throwable e) { /* Ignore */ } /* Fallback to the IP address padded to 6 bytes */ try { final byte[] hostAddress = Arrays.copyOfRange(InetAddress.getLocalHost().getAddress(), 0, 6); LOGGER.info("Hardware address is " + toHexString(hostAddress) + " (IP address)"); return hostAddress; } catch(final Throwable e) { /* Ignore */ } /* Fallback to a constant */ LOGGER.info("Hardware address is 00DEADBEEF00 (last resort)"); return new byte[] { (byte) 0x00, (byte) 0xDE, (byte) 0xAD, (byte) 0xBE, (byte) 0xEF, (byte) 0x00 }; } /** * Decides whether or nor a given MAC address is the address of some virtual interface, like e.g. VMware's host-only interface (server-side). * * @param addr * a MAC address * @return true if the MAC address is unsuitable as the device's hardware address */ public static boolean isBlockedHardwareAddress(final byte[] addr) { if((addr[0] & 0x02) != 0) /* Locally administered */ return true; else if((addr[0] == 0x00) && (addr[1] == 0x50) && (addr[2] == 0x56)) /* VMware */ return true; else if((addr[0] == 0x00) && (addr[1] == 0x1C) && (addr[2] == 0x42)) /* Parallels */ return true; else if((addr[0] == 0x00) && (addr[1] == 0x25) && (addr[2] == (byte) 0xAE)) /* Microsoft */ return true; else return false; } public static NSDictionary requestPList(final String username, final String password) throws Exception { final HttpURLConnection connection = (HttpURLConnection) new URL("https://homesharing.itunes.apple.com" + "/WebObjects/MZHomeSharing.woa/wa/getShareIdentifiers").openConnection(); connection.setAllowUserInteraction(false); connection.setDoInput(true); connection.setDoOutput(true); connection.setRequestProperty("Viewer-Only-Client", "1"); connection.setRequestProperty("User-Agent", "Remote/2.0"); connection.setRequestProperty("Accept-Encoding", "gzip"); connection.setRequestProperty("Connection", "keep-alive"); connection.setRequestProperty("Content-Type", "text/xml"); connection.setReadTimeout(0); final NSDictionary root = new NSDictionary(); root.put("appleId", username); root.put("guid", "empty"); root.put("password", password); final String xml = root.toXMLPropertyList(); connection.connect(); final OutputStream os = connection.getOutputStream(); final BufferedWriter writer = new BufferedWriter( new OutputStreamWriter(os, "UTF-8")); writer.write(xml); writer.flush(); writer.close(); os.close(); if(connection.getResponseCode() >= HttpURLConnection.HTTP_UNAUTHORIZED) throw new Exception("HTTP Error Response Code: " + connection.getResponseCode()); // obtain the encoding returned by the server final String encoding = connection.getContentEncoding(); final InputStream inputStream; // create the appropriate stream wrapper based on the encoding type if(encoding != null && encoding.equalsIgnoreCase("gzip")) { inputStream = new GZIPInputStream(connection.getInputStream()); } else if(encoding != null && encoding.equalsIgnoreCase("deflate")) { inputStream = new InflaterInputStream(connection.getInputStream(), new Inflater(true)); } else { inputStream = connection.getInputStream(); } final NSDictionary dictionary = (NSDictionary) PropertyListParser.parse(inputStream); final NSString o1 = (NSString) dictionary.get("spid"); final NSNumber o2 = (NSNumber) dictionary.get("status"); final NSNumber o3 = (NSNumber) dictionary.get("dsid"); final NSString o4 = (NSString) dictionary.get("sgid"); if(o1 == null && o3 == null && o4 == null && o2.intValue() == 5505) throw new Exception("bad password"); return dictionary; } }