/*******************************************************************************
* 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.dacp.client;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import javax.jmdns.JmDNS;
import javax.jmdns.NetworkTopologyEvent;
import javax.jmdns.ServiceEvent;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.dyndns.jkiddo.IDiscoverer;
import org.dyndns.jkiddo.dmcp.chunks.media.DeviceName;
import org.dyndns.jkiddo.dmcp.chunks.media.DeviceType;
import org.dyndns.jkiddo.dmcp.chunks.media.PairingContainer;
import org.dyndns.jkiddo.dmcp.chunks.media.PairingGuid;
import org.dyndns.jkiddo.dmp.util.DmapUtil;
import org.dyndns.jkiddo.service.dacp.server.ITouchAbleServerResource;
import org.dyndns.jkiddo.service.dmap.MDNSResource;
import org.dyndns.jkiddo.service.dmap.Util;
import org.dyndns.jkiddo.zeroconf.IZeroconfManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.sun.jersey.core.spi.factory.ResponseBuilderImpl;
@Singleton
public class TouchRemoteResource extends MDNSResource implements ITouchRemoteResource, IDiscoverer
{
public static final String DACP_CLIENT_PORT_NAME = "DACP_CLIENT_PORT_NAME";
public static final String DACP_CLIENT_PAIRING_CODE = "DACP_CLIENT_PAIRING_CODE";
private static final Logger LOGGER = LoggerFactory.getLogger(TouchRemoteResource.class);
private final IPairingDatabase database;
private final Integer actualCode;
private final String name;
private final IZeroconfManager mDNS;
private static MessageDigest md5;
static
{
try
{
md5 = MessageDigest.getInstance("MD5");
}
catch(final NoSuchAlgorithmException e)
{
LOGGER.error(e.getMessage(), e);
}
}
@Inject
public TouchRemoteResource(final IZeroconfManager mDNS, @Named(DACP_CLIENT_PORT_NAME) final Integer port, final IPairingDatabase database, @Named(DACP_CLIENT_PAIRING_CODE) final Integer code, @Named(Util.APPLICATION_NAME) final String applicationName) throws IOException
{
super(mDNS, port);
if(code < 0 || code > 9999)
throw new RuntimeException("Pairing code is not within required interval");
this.mDNS = mDNS;
this.actualCode = code;
this.database = database;
this.name = applicationName;
this.mDNS.addServiceListener(ITouchAbleServerResource.TOUCH_ABLE_SERVER, this);
this.mDNS.addNetworkTopologyListener(this);
this.register();
}
@Override
@GET
@Path("pair")
public Response pair(@Context final HttpServletRequest httpServletRequest, @Context final HttpServletResponse httpServletResponse, @QueryParam("pairingcode") final String pairingcode, @QueryParam("servicename") final String servicename) throws IOException
{
final byte[] code = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 };
final String match = expectedPairingCode(actualCode, database.getPairCode());
if(match.equals(pairingcode))
{
database.updateCode(servicename, Util.toHex(code));
final PairingContainer pc = new PairingContainer();
pc.add(new PairingGuid(code));
pc.add(new DeviceName("Jolivia’s iPhone"));
pc.add(new DeviceType("iPhone"));
return new ResponseBuilderImpl().entity(DmapUtil.serialize(pc, false)).status(Status.OK).build();
}
// TODO Response is not verified to be correct in iTunes regi - it is however better than nothing.
return new ResponseBuilderImpl().status(Status.UNAUTHORIZED).build();
}
@Override
protected IZeroconfManager.ServiceInfo getServiceInfoToRegister()
{
final Map<String, String> values = new HashMap<>();
values.put("DvNm", "Use " + actualCode + " as code for " + name);
values.put("RemV", "10000");
values.put("DvTy", "iPod");
values.put("RemN", "Remote");
values.put("txtvers", "1");
values.put("Pair", database.getPairCode());
return new IZeroconfManager.ServiceInfo(TOUCH_REMOTE_CLIENT, Util.toHex("JoliviaRemote"), this.port, values);
}
public static String expectedPairingCode(final Integer actualCode, final String databaseCode) throws IOException
{
final ByteArrayOutputStream os = new ByteArrayOutputStream();
os.write(databaseCode.getBytes("UTF-8"));
final byte[] codeAsBytes = String.format("%04d", actualCode).getBytes("UTF-8");
for (final byte codeAsByte : codeAsBytes) {
os.write(codeAsByte);
os.write(0);
}
return Util.toHex(md5.digest(os.toByteArray()));
}
@Override
public void serviceAdded(final ServiceEvent event)
{}
@Override
public void serviceRemoved(final ServiceEvent event)
{
LOGGER.info("REMOVE: " + event.getDNS().getServiceInfo(event.getType(), event.getName()));
try
{
final String code = database.findCode(event.getInfo().getName());
if(code != null)
{
database.updateCode(event.getInfo().getName(), null);
// Seems like someone removed our pairing ... make a re-announcement
this.register();
}
}
catch(final IOException e)
{
LOGGER.debug(e.getMessage(), e);
}
}
@Override
public void serviceResolved(final ServiceEvent event)
{}
@Override
public void inetAddressAdded(final NetworkTopologyEvent event)
{
final JmDNS mdns = event.getDNS();
final InetAddress address = event.getInetAddress();
LOGGER.info("Registered PairedRemoteDiscoverer @ " + address.getHostAddress());
mdns.addServiceListener(ITouchAbleServerResource.TOUCH_ABLE_SERVER, this);
}
@Override
public void inetAddressRemoved(final NetworkTopologyEvent event)
{
final JmDNS mdns = event.getDNS();
mdns.removeServiceListener(ITouchAbleServerResource.TOUCH_ABLE_SERVER, this);
mdns.unregisterAllServices();
}
}