/* * Copyright 2013 Hannes Janetzek * * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). * * This program is free software: you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) any later version. * * This program 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License along with * this program. If not, see <http://www.gnu.org/licenses/>. */ package org.oscim.tiling.source; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.Socket; import java.net.URL; import java.net.UnknownHostException; import java.util.Map.Entry; import java.util.zip.GZIPInputStream; import org.oscim.core.Tile; import org.oscim.utils.ArrayUtils; import org.oscim.utils.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Lightweight HTTP connection for tile loading. Does not do redirects, * https, full header parsing or other stuff. */ public class LwHttp implements HttpEngine { static final Logger log = LoggerFactory.getLogger(LwHttp.class); static final boolean dbg = false; private final static byte[] HEADER_HTTP_OK = "200 OK".getBytes(); private final static byte[] HEADER_CONTENT_LENGTH = "Content-Length".getBytes(); private final static byte[] HEADER_CONNECTION_CLOSE = "Connection: close".getBytes(); private final static byte[] HEADER_ENCODING_GZIP = "Content-Encoding: gzip".getBytes(); private final static int RESPONSE_EXPECTED_LIVES = 100; private final static long RESPONSE_TIMEOUT = (long) 10E9; // 10 second in nanosecond private final static int CONNECT_TIMEOUT = 15000; // 15 seconds private final static int SOCKET_TIMEOUT = 8000; // 8 seconds private final static int BUFFER_SIZE = 8192; private final byte[] buffer = new byte[BUFFER_SIZE]; private final String mHost; private final int mPort; private int mMaxRequests = 0; private Socket mSocket; private OutputStream mCommandStream; private Buffer mResponseStream; private long mLastRequest = 0; private InetSocketAddress mSockAddr; /** Server requested to close the connection */ private boolean mMustCloseConnection; private final byte[] REQUEST_GET_START; private final byte[] REQUEST_GET_END; private final byte[] mRequestBuffer; private final byte[][] mTilePath; private final UrlTileSource mTileSource; //private boolean mUseGZIP; private LwHttp(UrlTileSource tileSource, byte[][] tilePath) { mTilePath = tilePath; mTileSource = tileSource; URL url = tileSource.getUrl(); int port = url.getPort(); if (port < 0) port = 80; mHost = url.getHost(); mPort = port; String path = url.getPath(); REQUEST_GET_START = ("GET " + path).getBytes(); StringBuilder sb = new StringBuilder() .append(" HTTP/1.1") .append("\nUser-Agent: vtm/0.5.9") .append("\nHost: ") .append(mHost) .append("\nConnection: Keep-Alive"); for (Entry<String, String> l : tileSource.getRequestHeader().entrySet()) { String key = l.getKey(); String val = l.getValue(); //if ("Accept-Encoding".equals(key) && "gzip".equals(val)) // mUseGZIP = true; sb.append('\n').append(key).append(": ").append(val); } sb.append("\n\n"); REQUEST_GET_END = sb.toString().getBytes(); mRequestBuffer = new byte[1024]; System.arraycopy(REQUEST_GET_START, 0, mRequestBuffer, 0, REQUEST_GET_START.length); } static final class Buffer extends BufferedInputStream { OutputStream cache; int bytesRead = 0; int bytesWrote; int marked = -1; int contentLength; public Buffer(InputStream is) { super(is, BUFFER_SIZE); } public void setCache(OutputStream cache) { this.cache = cache; } public void start(int length) { bytesRead = 0; bytesWrote = 0; contentLength = length; } public boolean finishedReading() { try { while (bytesRead < contentLength && read() >= 0); } catch (IOException e) { log.debug(e.getMessage()); } return bytesRead == contentLength; } @Override public void close() throws IOException { if (dbg) log.debug("close()... ignored"); } @Override public synchronized void mark(int readlimit) { if (dbg) log.debug("mark {}", readlimit); marked = bytesRead; super.mark(readlimit); } @Override public synchronized long skip(long n) throws IOException { /* Android(4.1.2) image decoder *requires* skip to * actually skip the requested amount. * https://code.google.com/p/android/issues/detail?id=6066 */ long sumSkipped = 0L; while (sumSkipped < n) { long skipped = super.skip(n - sumSkipped); if (skipped != 0) { sumSkipped += skipped; continue; } if (read() < 0) break; // EOF sumSkipped += 1; /* was incremented by read() */ bytesRead -= 1; } if (dbg) log.debug("skip:{}/{} pos:{}", n, sumSkipped, bytesRead); bytesRead += sumSkipped; return sumSkipped; } @Override public synchronized void reset() throws IOException { if (dbg) log.debug("reset"); if (marked >= 0) bytesRead = marked; /* TODO could check if the mark is already invalid */ super.reset(); } @Override public int read() throws IOException { if (bytesRead >= contentLength) return -1; int data = super.read(); if (data >= 0) bytesRead += 1; if (cache != null && bytesRead > bytesWrote) { bytesWrote = bytesRead; cache.write(data); } return data; } @Override public int read(byte[] buffer, int offset, int byteCount) throws IOException { if (bytesRead >= contentLength) return -1; int len = super.read(buffer, offset, byteCount); if (dbg) log.debug("read {} {} {}", len, bytesRead, contentLength); if (len <= 0) return len; bytesRead += len; if (cache != null && bytesRead > bytesWrote) { int add = bytesRead - bytesWrote; bytesWrote = bytesRead; cache.write(buffer, offset + (len - add), add); } return len; } } private void checkSocket() throws IOException { if (mSocket == null) throw new IOException("No Socket"); } public synchronized InputStream read() throws IOException { checkSocket(); Buffer is = mResponseStream; is.mark(BUFFER_SIZE); is.start(BUFFER_SIZE); byte[] buf = buffer; boolean first = true; boolean gzip = false; int read = 0; int pos = 0; int end = 0; int len = 0; int contentLength = -1; /* header may not be larger than BUFFER_SIZE for this to work */ for (; (pos < read) || ((read < BUFFER_SIZE) && (len = is.read(buf, read, BUFFER_SIZE - read)) >= 0); len = 0) { read += len; /* end of header lines */ while (end < read && (buf[end] != '\n')) end++; if (end == BUFFER_SIZE) { throw new IOException("Header too large!"); } if (buf[end] != '\n') continue; /* empty line (header end) */ if (end - pos == 1) { end += 1; break; } if (first) { first = false; /* check only for OK ("HTTP/1.? ".length == 9) */ if (!check(HEADER_HTTP_OK, buf, pos + 9, end)) { throw new IOException("HTTP Error: " + new String(buf, pos, end - pos - 1)); } } else if (check(HEADER_CONTENT_LENGTH, buf, pos, end)) { /* parse Content-Length */ contentLength = parseInt(buf, pos + HEADER_CONTENT_LENGTH.length + 2, end - 1); } else if (check(HEADER_ENCODING_GZIP, buf, pos, end)) { gzip = true; } else if (check(HEADER_CONNECTION_CLOSE, buf, pos, end)) { mMustCloseConnection = true; } if (dbg) { String line = new String(buf, pos, end - pos - 1); log.debug("> {} <", line); } pos += (end - pos) + 1; end = pos; } /* back to start of content */ is.reset(); is.mark(0); is.skip(end); is.start(contentLength); if (gzip) { return new GZIPInputStream(is); } return is; } @Override public synchronized void sendRequest(Tile tile) throws IOException { if (mSocket != null) { if (--mMaxRequests < 0) close(); else if (System.nanoTime() - mLastRequest > RESPONSE_TIMEOUT) close(); else { try { int n = mResponseStream.available(); if (n > 0) { log.debug("left over bytes {} ", n); close(); } } catch (IOException e) { log.debug(e.getMessage()); close(); } } } if (mSocket == null) { /* might throw IOException */ lwHttpConnect(); /* TODO parse from header */ mMaxRequests = RESPONSE_EXPECTED_LIVES; } int pos = REQUEST_GET_START.length; int len = REQUEST_GET_END.length; pos = formatTilePath(tile, mRequestBuffer, pos); System.arraycopy(REQUEST_GET_END, 0, mRequestBuffer, pos, len); len += pos; if (dbg) log.debug("request: {}", new String(mRequestBuffer, 0, len)); try { writeRequest(len); } catch (IOException e) { log.debug("recreate connection"); close(); lwHttpConnect(); writeRequest(len); } } private void writeRequest(int length) throws IOException { mCommandStream.write(mRequestBuffer, 0, length); //mCommandStream.flush(); } private synchronized void lwHttpConnect() throws IOException { if (mSockAddr == null || mSockAddr.isUnresolved()) { mSockAddr = new InetSocketAddress(mHost, mPort); if (mSockAddr.isUnresolved()) throw new UnknownHostException(mHost); } try { mSocket = new Socket(); mSocket.setTcpNoDelay(true); mSocket.setSoTimeout(SOCKET_TIMEOUT); mSocket.connect(mSockAddr, CONNECT_TIMEOUT); mCommandStream = mSocket.getOutputStream(); mResponseStream = new Buffer(mSocket.getInputStream()); mMustCloseConnection = false; } catch (IOException e) { close(); throw e; } } @Override public void close() { IOUtils.closeQuietly(mSocket); synchronized (this) { mSocket = null; mCommandStream = null; mResponseStream = null; } } @Override public synchronized void setCache(OutputStream os) { if (mSocket == null) return; mResponseStream.setCache(os); } @Override public synchronized boolean requestCompleted(boolean ok) { if (mSocket == null) return false; mLastRequest = System.nanoTime(); mResponseStream.setCache(null); if (!ok || mMustCloseConnection || !mResponseStream.finishedReading()) close(); return ok; } /** write (positive) integer to byte array */ private static int writeInt(int val, int pos, byte[] buf) { if (val == 0) { buf[pos] = '0'; return pos + 1; } int i = 0; for (int n = val; n > 0; n = n / 10, i++) buf[pos + i] = (byte) ('0' + n % 10); ArrayUtils.reverse(buf, pos, pos + i); return pos + i; } /** parse (positive) integer from byte array */ private static int parseInt(byte[] buf, int pos, int end) { int val = 0; for (; pos < end; pos++) val = val * 10 + (buf[pos]) - '0'; return val; } private static boolean check(byte[] string, byte[] buffer, int position, int available) { int length = string.length; if (available - position < length) return false; for (int i = 0; i < length; i++) if (buffer[position + i] != string[i]) return false; return true; } /** * @param tile the Tile * @param buf to write url string * @param pos current position * @return new position */ private int formatTilePath(Tile tile, byte[] buf, int pos) { if (mTilePath == null) { String url = mTileSource.getUrlFormatter() .formatTilePath(mTileSource, tile); byte[] b = url.getBytes(); System.arraycopy(b, 0, buf, pos, b.length); return pos + b.length; } for (byte[] b : mTilePath) { if (b.length == 1) { if (b[0] == '/') { buf[pos++] = '/'; continue; } else if (b[0] == 'X') { pos = writeInt(tile.tileX, pos, buf); continue; } else if (b[0] == 'Y') { pos = writeInt(tile.tileY, pos, buf); continue; } else if (b[0] == 'Z') { pos = writeInt(tile.zoomLevel, pos, buf); continue; } } System.arraycopy(b, 0, buf, pos, b.length); pos += b.length; } return pos; } public static class LwHttpFactory implements HttpEngine.Factory { private byte[][] mTilePath; @Override public HttpEngine create(UrlTileSource tileSource) { if (tileSource.getUrlFormatter() != UrlTileSource.URL_FORMATTER) return new LwHttp(tileSource, null); /* use optimized formatter replacing the default */ if (mTilePath == null) { String[] path = tileSource.getTilePath(); mTilePath = new byte[path.length][]; for (int i = 0; i < path.length; i++) mTilePath[i] = path[i].getBytes(); } return new LwHttp(tileSource, mTilePath); } } }