/* ChromeCastHttpServer.java Copyright (c) 2014 NTT DOCOMO,INC. Released under the MIT license http://opensource.org/licenses/mit-license.php */ package org.deviceconnect.android.deviceplugin.chromecast.core; import android.content.Context; import android.net.ConnectivityManager; import android.net.NetworkInfo; import org.deviceconnect.android.deviceplugin.chromecast.BuildConfig; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.logging.Logger; import fi.iki.elonen.NanoHTTPD; /** * Chromecast HttpServer クラス. * * <p> * HttpServer機能を提供<br/> * - 選択されたファイルのみ配信する<br/> * </p> * @author NTT DOCOMO, INC. */ public class ChromeCastHttpServer extends NanoHTTPD { /** Local IP Address prefix. */ private static final String PREFIX_LOCAL_IP = "192.168."; enum NetworkStatus { OFF, WIFI, MOBILE, WIMAX, BLUETOOTH, ETHERNET; } /** Logger. */ private final Logger mLogger = Logger.getLogger("chromecast.dplugin"); /** The List of media files. */ private final List<MediaFile> mFileList = new ArrayList<MediaFile>(); private Context mContext; /** * コンストラクタ. * * @param host ipアドレス * @param port ポート番号 */ public ChromeCastHttpServer(final Context context, final String host, final int port) { super(host, port); mContext = context; } /** * クライアントに応答する. * * @param session セッション * @return レスポンス */ public Response serve(final IHTTPSession session) { mLogger.info("Received HTTP request: " + session.getUri()); Map<String, String> header = session.getHeaders(); String uri = session.getUri(); return respond(Collections.unmodifiableMap(header), session, uri); } /** * 指定されたファイルを公開する. * * @param file 公開するファイル * @return 公開用URI */ public String exposeFile(final MediaFile file) { synchronized (mFileList) { mFileList.add(file); } String address = getIpAddress(); if (address == null) { return null; } return "http://" + address + ":" + getListeningPort() + file.getPath(); } /** * クライアントをチェックする. * * @param headers ヘッダー * @return 有効か否か (true: 有効, false: 無効) */ private boolean checkRemote(final Map<String, String> headers) { String remoteAddr = headers.get("remote-addr"); try { InetAddress addr = InetAddress.getByName(remoteAddr); return addr.isSiteLocalAddress(); } catch (UnknownHostException e) { if (BuildConfig.DEBUG) { e.printStackTrace(); } return false; } } /** * クライアントへのレスポンスを作成する. * * @param headers ヘッダー * @param session セッション * @param uri ファイルのURI * @return レスポンス */ private Response respond(final Map<String, String> headers, final IHTTPSession session, final String uri) { if (!checkRemote(headers)) { return createResponse(Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT, ""); } MediaFile mediaFile = findFile(uri); if (mediaFile == null) { mLogger.info("File not found: URI=" + uri); return createResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, ""); } mLogger.info("Found File: " + mediaFile.mFile.getAbsolutePath()); Response response = serveFile(uri, headers, mediaFile.mFile, ""); if (response == null) { return createResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, ""); } return response; } /** * 指定したURIで公開しているファイルを検索する. * * @param uri ファイル公開用URI * @return 検索により見つかったファイル. 見つからなかった場合は<code>null</code> */ private MediaFile findFile(final String uri) { synchronized (mFileList) { for (MediaFile file : mFileList) { mLogger.info(" - " + file.getPath()); if (uri.equals(file.getPath())) { return file; } } } return null; } /** * レスポンスを作成する. * * @param status ステータス * @param mimeType MIMEタイプ * @param message メッセージ (InputStream) * @return レスポンス */ private Response createResponse(final Response.Status status, final String mimeType, final InputStream message) { Response res = new Response(status, mimeType, message); res.addHeader("Accept-Ranges", "bytes"); return res; } /** * レスポンスを作成する. * * @param status ステータス * @param mimeType MIMEタイプ * @param message メッセージ (String) * @return レスポンス */ private Response createResponse(final Response.Status status, final String mimeType, final String message) { Response res = new Response(status, mimeType, message); res.addHeader("Accept-Ranges", "bytes"); return res; } /** * IPアドレスを取得する. * * @return IPアドレス */ private String getIpAddress() { try { Enumeration<NetworkInterface> networkInterfaces = NetworkInterface .getNetworkInterfaces(); LinkedList<InetAddress> localAddresses = new LinkedList<InetAddress>(); while (networkInterfaces.hasMoreElements()) { NetworkInterface networkInterface = (NetworkInterface) networkInterfaces .nextElement(); Enumeration<InetAddress> ipAddrs = networkInterface .getInetAddresses(); while (ipAddrs.hasMoreElements()) { InetAddress ip = (InetAddress) ipAddrs.nextElement(); String ipStr = ip.getHostAddress(); mLogger.info("Searching IP Address: Address=" + ipStr + " isLoopback=" + ip.isLoopbackAddress() + " isSiteLocal=" + ip.isSiteLocalAddress()); NetworkStatus status = NetworkStatus.OFF; NetworkInfo info = ((ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo(); if ( info != null ) { if ( info.isConnected() ) { switch ( info.getType() ) { case ConnectivityManager.TYPE_WIFI: // Wifi status = NetworkStatus.WIFI; break; case ConnectivityManager.TYPE_MOBILE_DUN: // Mobile 3G case ConnectivityManager.TYPE_MOBILE_HIPRI: case ConnectivityManager.TYPE_MOBILE_MMS: case ConnectivityManager.TYPE_MOBILE_SUPL: case ConnectivityManager.TYPE_MOBILE: status = NetworkStatus.MOBILE; break; case ConnectivityManager.TYPE_BLUETOOTH: // Bluetooth status = NetworkStatus.BLUETOOTH; break; case ConnectivityManager.TYPE_ETHERNET: // Ethernet status = NetworkStatus.ETHERNET; break; } } } // ipv6を除外 if (ipStr.indexOf("127.0.0.1") == -1 && ipStr.indexOf("::") == -1 && (status == NetworkStatus.WIFI || status == NetworkStatus.ETHERNET)) { localAddresses.addFirst(ip); } } } if (localAddresses.size() == 0) { return null; } return localAddresses.get(0).getHostAddress(); } catch (SocketException e) { if (BuildConfig.DEBUG) { e.printStackTrace(); } } return null; } /** * ファイルのレスポンスを作成する. * * @param uri ファイルのURI * @param header ヘッダー * @param file ファイル * @param mime MIMEタイプ * @return レスポンス */ Response serveFile(final String uri, final Map<String, String> header, final File file, final String mime) { Response res; try { String etag = Integer.toHexString((file.getAbsolutePath() + file.lastModified() + "" + file.length()).hashCode()); long startFrom = 0; long endAt = -1; String range = header.get("range"); if (range != null) { if (range.startsWith("bytes=")) { range = range.substring("bytes=".length()); int minus = range.indexOf('-'); try { if (minus > 0) { startFrom = Long.parseLong(range .substring(0, minus)); endAt = Long.parseLong(range.substring(minus + 1)); } } catch (NumberFormatException ignored) { if (BuildConfig.DEBUG) { ignored.printStackTrace(); } } } } long fileLen = file.length(); if (range != null && startFrom >= 0) { if (startFrom >= fileLen) { res = createResponse(Response.Status.RANGE_NOT_SATISFIABLE, NanoHTTPD.MIME_PLAINTEXT, ""); res.addHeader("Content-Range", "bytes 0-0/" + fileLen); res.addHeader("ETag", etag); } else { if (endAt < 0) { endAt = fileLen - 1; } long newLen = endAt - startFrom + 1; if (newLen < 0) { newLen = 0; } final long dataLen = newLen; FileInputStream fis = new FileInputStream(file) { @Override public int available() throws IOException { return (int) dataLen; } }; fis.skip(startFrom); res = createResponse(Response.Status.PARTIAL_CONTENT, mime, fis); res.addHeader("Content-Length", "" + dataLen); res.addHeader("Content-Range", "bytes " + startFrom + "-" + endAt + "/" + fileLen); res.addHeader("ETag", etag); } } else { if (etag.equals(header.get("if-none-match"))) { res = createResponse(Response.Status.NOT_MODIFIED, mime, ""); } else { res = createResponse(Response.Status.OK, mime, new FileInputStream(file)); res.addHeader("Content-Length", "" + fileLen); res.addHeader("ETag", etag); } } } catch (IOException ioe) { res = createResponse(Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT, ""); } return res; } }