// Remote
//
// Douglas
//
// Remote.java
/* Copyright 2011 Douglas Thrift
*
* This file is part of Big Screen Bot.
*
* Big Screen Bot is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Big Screen Bot is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Big Screen Bot. If not, see .
*/
package net.douglasthrift.bigscreenbot;
import java.io.Closeable;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.UnknownHostException;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.KeyManagementException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.jmdns.JmDNS;
import javax.jmdns.ServiceInfo;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import com.google.anymote.common.AnymoteFactory;
import com.google.anymote.common.ErrorListener;
import com.google.anymote.device.DeviceAdapter;
import com.google.anymote.device.MessageReceiver;
import com.google.polo.exception.PoloException;
import com.google.polo.pairing.ClientPairingSession;
import com.google.polo.pairing.PairingContext;
import com.google.polo.pairing.PairingListener;
import com.google.polo.pairing.PairingSession;
import com.google.polo.pairing.message.EncodingOption;
import com.google.polo.ssl.DummySSLSocketFactory;
import com.google.polo.ssl.SslUtil;
import com.google.polo.wire.WireFormat;
public class Remote implements Closeable
{
private static final String STORE = "bigscreenbot.keystore";
private static final char[] PASSWORD = "b1GsSC33Nb0T".toCharArray();
private static final char[] NULL = new char[]{};
private static final String TYPE = "_anymote._tcp.local.";
private static final String LOCAL_ALIAS = "anymote-remote";
private static final String REMOTE_ALIAS = "anymote-server-%1$X";
private static class Secret
{
private String secret;
private Thread thread;
public Secret()
{
thread = Thread.currentThread();
}
public synchronized void set(String secret)
{
this.secret = secret;
notify();
}
public synchronized String get() throws InterruptedException
{
while (secret == null)
wait();
return secret;
}
public void interrupt()
{
thread.interrupt();
}
}
static
{
new Fixer();
}
private boolean verbose;
private Settings settings;
private JmDNS mdns;
private KeyStore store;
private SSLSocketFactory factory;
private Map secrets = Collections.synchronizedMap(new HashMap());
public Remote(boolean verbose, Settings settings) throws UnknownHostException, IOException, KeyStoreException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException, KeyManagementException, GeneralSecurityException
{
this.verbose = verbose;
this.settings = settings;
String interfaze = settings.getProperty("interface");
if (interfaze != null)
mdns = JmDNS.create(InetAddress.getByName(interfaze));
else
mdns = JmDNS.create();
store = KeyStore.getInstance(KeyStore.getDefaultType());
FileInputStream stream = null;
try
{
stream = new FileInputStream(STORE);
store.load(stream, PASSWORD);
}
catch (FileNotFoundException exception)
{
store.load(null, PASSWORD);
}
finally
{
if (stream != null)
stream.close();
}
if (!store.containsAlias(LOCAL_ALIAS))
{
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
KeyPair pair = generator.generateKeyPair();
X509Certificate certificate = SslUtil.generateX509V3Certificate(pair, "CN=anymote/bigscreenbot/" + System.getProperty("os.name") + "/" + System.getProperty("os.arch") + "/" + System.getProperty("user.name") + "@" + InetAddress.getLocalHost().getHostName());
store.setKeyEntry(LOCAL_ALIAS, pair.getPrivate(), NULL, new Certificate[]{ certificate });
store();
}
KeyManagerFactory factory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
factory.init(store, NULL);
this.factory = DummySSLSocketFactory.fromKeyManagers(factory.getKeyManagers());
}
@Override
public void close() throws IOException
{
mdns.close();
}
public List listDevices()
{
List devices = new ArrayList();
for (ServiceInfo info : mdns.list(TYPE))
devices.add(info.getName());
return devices;
}
public boolean startPairDevice(String device, final Runnable runnable) throws MalformedURLException, UnknownHostException, IOException, PoloException, KeyStoreException, NoSuchAlgorithmException, CertificateException
{
SSLSocket socket = (SSLSocket)factory.createSocket();
final InetSocketAddress address = getDevice(device);
socket.connect(address);
PairingContext context = PairingContext.fromSslSocket(socket, false);
ClientPairingSession session = new ClientPairingSession(WireFormat.PROTOCOL_BUFFERS.getWireInterface(context), context, "AnyMote", "Big Screen Bot");
EncodingOption option = new EncodingOption(EncodingOption.EncodingType.ENCODING_HEXADECIMAL, 4);
session.addInputEncoding(option);
session.addOutputEncoding(option);
if (session.doPair(new PairingListener()
{
@Override
public void onLogMessage(LogLevel level, String message)
{
if (verbose)
System.out.println(level + ": " + message);
}
@Override
public void onPerformInputDeviceRole(PairingSession session)
{
runnable.run();
Secret secret = new Secret(), oldSecret = secrets.put(address, secret);
if (oldSecret != null)
oldSecret.interrupt();
try
{
session.setSecret(session.getEncoder().decodeToBytes(secret.get()));
}
catch (InterruptedException exception)
{
session.teardown();
}
}
@Override
public void onPerformOutputDeviceRole(PairingSession session, byte[] gamma) throws PoloException
{
throw new PoloException("This should not happen!");
}
@Override
public void onSessionCreated(PairingSession session)
{
if (verbose)
System.out.println(session + " created.");
}
@Override
public void onSessionEnded(PairingSession session)
{
if (verbose)
System.out.println(session + " ended.");
}
}))
{
synchronized (store)
{
Certificate certificate = context.getServerCertificate();
String alias = String.format(REMOTE_ALIAS, certificate.hashCode());
if (store.containsAlias(alias))
store.deleteEntry(alias);
store.setCertificateEntry(alias, certificate);
store();
}
return true;
}
else
return false;
}
public boolean finishPairDevice(String device, String code) throws MalformedURLException
{
Secret secret = secrets.remove(getDevice(device));
if (secret != null)
secret.set(code);
else
return false;
return true;
}
public void fling(String url)
{
}
public void fling(String device, String url)
{
}
private InetSocketAddress getDevice(String device) throws MalformedURLException
{
ServiceInfo info = mdns.getServiceInfo(TYPE, device);
if (info != null)
return new InetSocketAddress(info.getInetAddresses()[0], info.getPort());
else
{
URL url = new URL("http://" + device);
int port = url.getPort();
return new InetSocketAddress(url.getHost(), port == -1 ? 9551 : port);
}
}
private void store() throws FileNotFoundException, IOException, KeyStoreException, NoSuchAlgorithmException, CertificateException
{
FileOutputStream stream = null;
try
{
stream = new FileOutputStream(STORE);
store.store(stream, PASSWORD);
}
finally
{
if (stream != null)
stream.close();
}
}
}
// vim: expandtab