// 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