You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1037 lines
45 KiB
1037 lines
45 KiB
// httpdProxyHandler.java
|
|
// -----------------------
|
|
// part of YACY
|
|
// (C) by Michael Peter Christen; mc@anomic.de
|
|
// first published on http://www.anomic.de
|
|
// Frankfurt, Germany, 2004
|
|
// last major change: 10.05.2004
|
|
//
|
|
// This program 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 2 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 General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program; if not, write to the Free Software
|
|
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
//
|
|
// Using this software in any meaning (reading, learning, copying, compiling,
|
|
// running) means that you agree that the Author(s) is (are) not responsible
|
|
// for cost, loss of data or any harm that may be caused directly or indirectly
|
|
// by usage of this softare or this documentation. The usage of this software
|
|
// is on your own risk. The installation and usage (starting/running) of this
|
|
// software may allow other people or application to access your computer and
|
|
// any attached devices and is highly dependent on the configuration of the
|
|
// software which must be done by the user of the software; the author(s) is
|
|
// (are) also not responsible for proper configuration and usage of the
|
|
// software, even if provoked by documentation provided together with
|
|
// the software.
|
|
//
|
|
// Any changes to this file according to the GPL as documented in the file
|
|
// gpl.txt aside this file in the shipment you received can be done to the
|
|
// lines that follows this copyright notice here, but changes must not be
|
|
// done inside the copyright notive above. A re-distribution must contain
|
|
// the intact and unchanged copyright notice.
|
|
// Contributions and changes to the program code must be marked as such.
|
|
|
|
// Contributions:
|
|
// [AS] Alexander Schier: Blacklist (404 response for AGIS hosts)
|
|
// [TL] Timo Leise: url-wildcards for blacklists
|
|
|
|
/*
|
|
Class documentation:
|
|
This class is a servlet to the httpd daemon. It is accessed each time
|
|
an URL in a GET, HEAD or POST command contains the whole host information
|
|
or a host is given in the header host field of an HTTP/1.0 / HTTP/1.1
|
|
command.
|
|
Transparency is maintained, whenever appropriate. We change header
|
|
atributes if necessary for the indexing mechanism; i.e. we do not
|
|
support gzip-ed encoding. We also do not support unrealistic
|
|
'expires' values that would force a cache to be flushed immediately
|
|
pragma non-cache attributes are supported
|
|
*/
|
|
|
|
|
|
package de.anomic.http;
|
|
|
|
import java.io.BufferedReader;
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.InputStreamReader;
|
|
import java.io.OutputStream;
|
|
import java.io.PrintStream;
|
|
import java.io.PushbackInputStream;
|
|
import java.net.MalformedURLException;
|
|
import java.net.Socket;
|
|
import java.net.SocketException;
|
|
import java.net.URL;
|
|
import java.util.Date;
|
|
import java.util.HashSet;
|
|
import java.util.Iterator;
|
|
import java.util.Properties;
|
|
import java.util.TreeMap;
|
|
|
|
import de.anomic.htmlFilter.htmlFilterContentScraper;
|
|
import de.anomic.htmlFilter.htmlFilterContentTransformer;
|
|
import de.anomic.htmlFilter.htmlFilterOutputStream;
|
|
import de.anomic.htmlFilter.htmlFilterTransformer;
|
|
import de.anomic.plasma.plasmaCrawlLURL;
|
|
import de.anomic.plasma.plasmaHTCache;
|
|
import de.anomic.plasma.plasmaSwitchboard;
|
|
import de.anomic.server.serverCore;
|
|
import de.anomic.server.serverFileUtils;
|
|
import de.anomic.server.serverLog;
|
|
import de.anomic.server.serverObjects;
|
|
import de.anomic.server.serverSwitch;
|
|
import de.anomic.yacy.yacyCore;
|
|
|
|
|
|
public final class httpdProxyHandler extends httpdAbstractHandler implements httpdHandler {
|
|
|
|
// static variables
|
|
// can only be instantiated upon first instantiation of this class object
|
|
private static plasmaSwitchboard switchboard = null;
|
|
private static plasmaHTCache cacheManager = null;
|
|
public static serverLog log;
|
|
public static HashSet yellowList = null;
|
|
public static TreeMap blackListURLs = null;
|
|
private static int timeout = 30000;
|
|
private static boolean yacyTrigger = true;
|
|
|
|
public static boolean isTransparentProxy = false;
|
|
|
|
public static boolean remoteProxyUse = false;
|
|
public static String remoteProxyHost = "";
|
|
public static int remoteProxyPort = -1;
|
|
public static String remoteProxyNoProxy = "";
|
|
public static String[] remoteProxyNoProxyPatterns = null;
|
|
private static final HashSet remoteProxyAllowProxySet = new HashSet();
|
|
private static final HashSet remoteProxyDisallowProxySet = new HashSet();
|
|
|
|
|
|
private static htmlFilterTransformer transformer = null;
|
|
public static final String userAgent = "yacy (" + httpc.systemOST +") yacy.net";
|
|
private File htRootPath = null;
|
|
|
|
// class methods
|
|
public httpdProxyHandler(serverSwitch sb) {
|
|
if (switchboard == null) {
|
|
switchboard = (plasmaSwitchboard) sb;
|
|
cacheManager = switchboard.getCacheManager();
|
|
|
|
// load remote proxy data
|
|
remoteProxyHost = switchboard.getConfig("remoteProxyHost","");
|
|
try {
|
|
remoteProxyPort = Integer.parseInt(switchboard.getConfig("remoteProxyPort","3128"));
|
|
} catch (NumberFormatException e) {
|
|
remoteProxyPort = 3128;
|
|
}
|
|
remoteProxyUse = switchboard.getConfig("remoteProxyUse","false").equals("true");
|
|
remoteProxyNoProxy = switchboard.getConfig("remoteProxyNoProxy","");
|
|
remoteProxyNoProxyPatterns = remoteProxyNoProxy.split(",");
|
|
|
|
// set loglevel
|
|
int loglevel = Integer.parseInt(switchboard.getConfig("proxyLoglevel", "2"));
|
|
log = new serverLog("HTTPDProxy", loglevel);
|
|
|
|
// set timeout
|
|
timeout = Integer.parseInt(switchboard.getConfig("clientTimeout", "10000"));
|
|
|
|
// create a htRootPath: system pages
|
|
if (htRootPath == null) {
|
|
htRootPath = new File(switchboard.getRootPath(), switchboard.getConfig("htRootPath","htroot"));
|
|
if (!(htRootPath.exists())) htRootPath.mkdir();
|
|
}
|
|
|
|
// load a transformer
|
|
transformer = new htmlFilterContentTransformer();
|
|
transformer.init(new File(switchboard.getRootPath(), switchboard.getConfig("plasmaBlueList", "")).toString());
|
|
|
|
String f;
|
|
// load the yellow-list
|
|
f = switchboard.getConfig("proxyYellowList", null);
|
|
if (f != null) yellowList = loadSet("yellow", f); else yellowList = new HashSet();
|
|
|
|
// load the black-list / inspired by [AS]
|
|
f = switchboard.getConfig("proxyBlackListsActive", null);
|
|
if (f != null) blackListURLs = loadBlacklist("black", f, "/"); else blackListURLs = new TreeMap();
|
|
log.logSystem("Proxy Handler Initialized");
|
|
}
|
|
}
|
|
|
|
|
|
private static HashSet loadSet(String setname, String filename) {
|
|
HashSet set = new HashSet();
|
|
BufferedReader br = null;
|
|
try {
|
|
br = new BufferedReader(new InputStreamReader(new FileInputStream(filename)));
|
|
String line;
|
|
while ((line = br.readLine()) != null) {
|
|
line = line.trim();
|
|
if ((line.length() > 0) && (!(line.startsWith("#")))) set.add(line.trim().toLowerCase());
|
|
}
|
|
br.close();
|
|
serverLog.logInfo("PROXY", "read " + setname + " set from file " + filename);
|
|
} catch (IOException e) {
|
|
} finally {
|
|
if (br != null) try { br.close(); } catch (Exception e) {}
|
|
}
|
|
return set;
|
|
}
|
|
|
|
private static TreeMap loadMap(String mapname, String filename, String sep) {
|
|
TreeMap map = new TreeMap();
|
|
BufferedReader br = null;
|
|
try {
|
|
br = new BufferedReader(new InputStreamReader(new FileInputStream(filename)));
|
|
String line;
|
|
int pos;
|
|
while ((line = br.readLine()) != null) {
|
|
line = line.trim();
|
|
if ((line.length() > 0) && (!(line.startsWith("#"))) && ((pos = line.indexOf(sep)) > 0))
|
|
map.put(line.substring(0, pos).trim().toLowerCase(), line.substring(pos + sep.length()).trim());
|
|
}
|
|
serverLog.logInfo("PROXY", "read " + mapname + " map from file " + filename);
|
|
} catch (IOException e) {
|
|
} finally {
|
|
if (br != null) try { br.close(); } catch (Exception e) {}
|
|
}
|
|
return map;
|
|
}
|
|
|
|
public static TreeMap loadBlacklist(String mapname, String filenames, String sep) {
|
|
TreeMap map = new TreeMap();
|
|
if (switchboard == null) return map; // not initialized yet
|
|
File listsPath = new File(switchboard.getRootPath(), switchboard.getConfig("listsPath", "DATA/LISTS"));
|
|
String filenamesarray[] = filenames.split(",");
|
|
|
|
if(filenamesarray.length >0)
|
|
for(int i = 0; i < filenamesarray.length; i++)
|
|
map.putAll(loadMap(mapname, (new File(listsPath, filenamesarray[i])).toString(), sep));
|
|
return map;
|
|
}
|
|
|
|
private static String domain(String host) {
|
|
String domain = host;
|
|
int pos = domain.lastIndexOf(".");
|
|
if (pos >= 0) {
|
|
// truncate from last part
|
|
domain = domain.substring(0, pos);
|
|
pos = domain.lastIndexOf(".");
|
|
if (pos >= 0) {
|
|
// truncate from first part
|
|
domain = domain.substring(pos + 1);
|
|
}
|
|
}
|
|
return domain;
|
|
}
|
|
|
|
private boolean blacklistedURL(String hostlow, String path) {
|
|
if (blackListURLs == null) return false;
|
|
|
|
int index = 0;
|
|
|
|
// [TL] While "." are found within the string
|
|
while ((index = hostlow.indexOf(".", index + 1)) != -1) {
|
|
if (blackListURLs.get(hostlow.substring(0, index + 1) + "*") != null) {
|
|
//System.out.println("Host blocked: " + hostlow.substring(0, index+1) + "*");
|
|
return true;
|
|
}
|
|
}
|
|
|
|
index = hostlow.length();
|
|
while ((index = hostlow.lastIndexOf(".", index - 1)) != -1) {
|
|
if (blackListURLs.get("*" + hostlow.substring(index, hostlow.length())) != null) {
|
|
//System.out.println("Host blocked: " + "*" + hostlow.substring(index, host.length()));
|
|
return true;
|
|
}
|
|
}
|
|
|
|
String pp = ""; // path-pattern
|
|
return (((pp = (String) blackListURLs.get(hostlow)) != null) &&
|
|
((pp.equals("*")) || (path.substring(1).matches(pp))));
|
|
}
|
|
|
|
public void handleOutgoingCookies(httpHeader requestHeader, String targethost, String clienthost) {
|
|
/*
|
|
The syntax for the header is:
|
|
|
|
cookie = "Cookie:" cookie-version
|
|
1*((";" | ",") cookie-value)
|
|
cookie-value = NAME "=" VALUE [";" path] [";" domain]
|
|
cookie-version = "$Version" "=" value
|
|
NAME = attr
|
|
VALUE = value
|
|
path = "$Path" "=" value
|
|
domain = "$Domain" "=" value
|
|
*/
|
|
if (requestHeader.containsKey(httpHeader.COOKIE)) {
|
|
Object[] entry = new Object[]{new Date(), clienthost, requestHeader.getMultiple(httpHeader.COOKIE)};
|
|
switchboard.outgoingCookies.put(targethost, entry);
|
|
}
|
|
}
|
|
|
|
public void handleIncomingCookies(httpHeader respondHeader, String serverhost, String targetclient) {
|
|
/*
|
|
The syntax for the Set-Cookie response header is
|
|
|
|
set-cookie = "Set-Cookie:" cookies
|
|
cookies = 1#cookie
|
|
cookie = NAME "=" VALUE *(";" cookie-av)
|
|
NAME = attr
|
|
VALUE = value
|
|
cookie-av = "Comment" "=" value
|
|
| "Domain" "=" value
|
|
| "Max-Age" "=" value
|
|
| "Path" "=" value
|
|
| "Secure"
|
|
| "Version" "=" 1*DIGIT
|
|
*/
|
|
if (respondHeader.containsKey(httpHeader.SET_COOKIE)) {
|
|
Object[] entry = new Object[]{new Date(), targetclient, respondHeader.getMultiple(httpHeader.SET_COOKIE)};
|
|
switchboard.incomingCookies.put(serverhost, entry);
|
|
}
|
|
}
|
|
|
|
public void doGet(Properties conProp, httpHeader requestHeader, OutputStream respond) throws IOException {
|
|
// prepare response
|
|
// conProp : a collection of properties about the connection, like URL
|
|
// requestHeader : The header lines of the connection from the request
|
|
// args : the argument values of a connection, like &-values in GET and values within boundaries in POST
|
|
// files : files within POST boundaries, same key as in args
|
|
|
|
if (yacyTrigger) de.anomic.yacy.yacyCore.triggerOnlineAction();
|
|
|
|
Date requestDate = new Date(); // remember the time...
|
|
String method = conProp.getProperty("METHOD");
|
|
String host = conProp.getProperty("HOST");
|
|
String path = conProp.getProperty("PATH"); // always starts with leading '/'
|
|
String args = conProp.getProperty("ARGS"); // may be null if no args were given
|
|
String ip = conProp.getProperty("CLIENTIP"); // the ip from the connecting peer
|
|
|
|
int port;
|
|
int pos;
|
|
|
|
if ((pos = host.indexOf(":")) < 0) {
|
|
port = 80;
|
|
} else {
|
|
port = Integer.parseInt(host.substring(pos + 1));
|
|
host = host.substring(0, pos);
|
|
}
|
|
|
|
String ext;
|
|
if ((pos = path.lastIndexOf('.')) < 0) {
|
|
ext = "";
|
|
} else {
|
|
ext = path.substring(pos + 1).toLowerCase();
|
|
}
|
|
|
|
URL url = null;
|
|
try {
|
|
if (args == null)
|
|
url = new URL("http", host, port, path);
|
|
else
|
|
url = new URL("http", host, port, path + "?" + args);
|
|
} catch (MalformedURLException e) {
|
|
serverLog.logError("PROXY", "ERROR: internal error with url generation: host=" +
|
|
host + ", port=" + port + ", path=" + path + ", args=" + args);
|
|
url = null;
|
|
}
|
|
//System.out.println("GENERATED URL:" + url.toString()); // debug
|
|
|
|
// check the blacklist
|
|
// blacklist idea inspired by [AS]:
|
|
// respond a 404 for all AGIS ("all you get is shit") servers
|
|
String hostlow = host.toLowerCase();
|
|
if (blacklistedURL(hostlow, path)) {
|
|
try {
|
|
respondHeader(respond,"404 Not Found (AGIS)", new httpHeader(null));
|
|
respond.write(("404 (generated): URL '" + hostlow + "' blocked by yacy proxy (blacklisted)\r\n").getBytes());
|
|
respond.flush();
|
|
serverLog.logInfo("PROXY", "AGIS blocking of host '" + hostlow + "'"); // debug
|
|
return;
|
|
} catch (Exception ee) {}
|
|
}
|
|
|
|
// handle outgoing cookies
|
|
handleOutgoingCookies(requestHeader, host, ip);
|
|
|
|
// set another userAgent, if not yellowlisted
|
|
if ((yellowList != null) && (!(yellowList.contains(domain(hostlow))))) {
|
|
// change the User-Agent
|
|
requestHeader.put(httpHeader.USER_AGENT, userAgent);
|
|
}
|
|
|
|
// set a scraper and a htmlFilter
|
|
OutputStream hfos = null;
|
|
htmlFilterContentScraper scraper = null;
|
|
|
|
// resolve yacy and yacyh domains
|
|
String yAddress = yacyCore.seedDB.resolveYacyAddress(host);
|
|
|
|
// re-calc the url path
|
|
String remotePath = (args == null) ? path : (path + "?" + args); // with leading '/'
|
|
|
|
// attach possible yacy-sublevel-domain
|
|
if ((yAddress != null) &&
|
|
((pos = yAddress.indexOf("/")) >= 0) &&
|
|
(!(remotePath.startsWith("/env"))) // this is the special path, staying always at root-level
|
|
) remotePath = yAddress.substring(pos) + remotePath;
|
|
|
|
// decide wether to use a cache entry or connect to the network
|
|
File cacheFile = cacheManager.getCachePath(url);
|
|
String urlHash = plasmaCrawlLURL.urlHash(url);
|
|
httpHeader cachedResponseHeader = null;
|
|
boolean cacheExists = ((cacheFile.exists()) && (cacheFile.isFile()) &&
|
|
((cachedResponseHeader = cacheManager.getCachedResponse(urlHash)) != null));
|
|
|
|
// why are files unzipped upon arrival? why not zip all files in cache?
|
|
// This follows from the following premises
|
|
// (a) no file shall be unzip-ed more than once to prevent unnessesary computing time
|
|
// (b) old cache entries shall be comparable with refill-entries to detect/distiguish case 3+4
|
|
// (c) the indexing mechanism needs files unzip-ed, a schedule could do that later
|
|
// case b and c contradicts, if we use a scheduler, because files in a stale cache would be unzipped
|
|
// and the newly arrival would be zipped and would have to be unzipped upon load. But then the
|
|
// scheduler is superfluous. Therefore the only reminding case is
|
|
// (d) cached files shall be either all zipped or unzipped
|
|
// case d contradicts with a, because files need to be unzipped for indexing. Therefore
|
|
// the only remaining case is to unzip files right upon load. Thats what we do here.
|
|
|
|
// finally use existing cache if appropriate
|
|
// here we must decide weather or not to save the data
|
|
// to a cache
|
|
// we distinguish four CACHE STATE cases:
|
|
// 1. cache fill
|
|
// 2. cache fresh - no refill
|
|
// 3. cache stale - refill - necessary
|
|
// 4. cache stale - refill - superfluous
|
|
// in two of these cases we trigger a scheduler to handle newly arrived files:
|
|
// case 1 and case 3
|
|
plasmaHTCache.Entry cacheEntry;
|
|
if ((cacheExists) &&
|
|
((cacheEntry = cacheManager.newEntry(requestDate, 0, url, requestHeader, "200 OK",
|
|
cachedResponseHeader, null,
|
|
switchboard.defaultProxyProfile)).shallUseCache())) {
|
|
// we respond on the request by using the cache, the cache is fresh
|
|
|
|
try {
|
|
// replace date field in old header by actual date, this is according to RFC
|
|
cachedResponseHeader.put(httpHeader.DATE, httpc.dateString(httpc.nowDate()));
|
|
|
|
// maybe the content length is missing
|
|
if (!(cachedResponseHeader.containsKey(httpHeader.CONTENT_LENGTH)))
|
|
cachedResponseHeader.put(httpHeader.CONTENT_LENGTH, Long.toString(cacheFile.length()));
|
|
|
|
// check if we can send a 304 instead the complete content
|
|
if (requestHeader.containsKey(httpHeader.IF_MODIFIED_SINCE)) {
|
|
// conditional request: freshness of cache for that condition was already
|
|
// checked within shallUseCache(). Now send only a 304 response
|
|
log.logInfo("CACHE HIT/304 " + cacheFile.toString());
|
|
|
|
// send cached header with replaced date and added length
|
|
respondHeader(respond, "304 OK", cachedResponseHeader); // respond with 'not modified'
|
|
|
|
} else {
|
|
// unconditional request: send content of cache
|
|
log.logInfo("CACHE HIT/203 " + cacheFile.toString());
|
|
|
|
// send cached header with replaced date and added length
|
|
respondHeader(respond, "203 OK", cachedResponseHeader); // respond with 'non-authoritative'
|
|
|
|
// make a transformer
|
|
if ((!(transformer.isIdentityTransformer())) &&
|
|
((ext == null) || (!(switchboard.extensionBlack.contains(ext)))) &&
|
|
((cachedResponseHeader == null) || (httpd.isTextMime(cachedResponseHeader.mime(), switchboard.mimeWhite)))) {
|
|
hfos = new htmlFilterOutputStream(respond, null, transformer, (ext.length() == 0));
|
|
} else {
|
|
hfos = respond;
|
|
}
|
|
|
|
// send also the complete body now from the cache
|
|
// simply read the file and transfer to out socket
|
|
InputStream is = null;
|
|
try {
|
|
is = new FileInputStream(cacheFile);
|
|
byte[] buffer = new byte[2048];
|
|
int l;
|
|
while ((l = is.read(buffer)) > 0) {hfos.write(buffer, 0, l);}
|
|
} finally {
|
|
if (is != null) try { is.close(); } catch (Exception e) {}
|
|
}
|
|
if (hfos instanceof htmlFilterOutputStream) ((htmlFilterOutputStream) hfos).finalize();
|
|
}
|
|
// that's it!
|
|
} catch (SocketException e) {
|
|
// this happens if the client stops loading the file
|
|
// we do nothing here
|
|
respondError(respond, "111 socket error: " + e.getMessage(), 1, url.toString());
|
|
}
|
|
respond.flush();
|
|
return;
|
|
}
|
|
|
|
// the cache does either not exist or is (supposed to be) stale
|
|
long sizeBeforeDelete = -1;
|
|
if (cacheExists) {
|
|
// delete the cache
|
|
sizeBeforeDelete = cacheFile.length();
|
|
cacheFile.delete();
|
|
}
|
|
|
|
// take a new file from the server
|
|
httpc remote = null;
|
|
httpc.response res = null;
|
|
|
|
try {
|
|
// open the connection
|
|
if (yAddress == null) {
|
|
remote = newhttpc(host, port, timeout);
|
|
} else {
|
|
remote = newhttpc(yAddress, timeout);
|
|
}
|
|
//System.out.println("HEADER: CLIENT TO PROXY = " + requestHeader.toString()); // DEBUG
|
|
|
|
// send request
|
|
res = remote.GET(remotePath, requestHeader);
|
|
long contentLength = res.responseHeader.contentLength();
|
|
|
|
// reserver cache entry
|
|
cacheEntry = cacheManager.newEntry(requestDate, 0, url, requestHeader, res.status, res.responseHeader, null, switchboard.defaultProxyProfile);
|
|
|
|
// handle file types
|
|
if (((ext == null) || (!(switchboard.extensionBlack.contains(ext)))) &&
|
|
(httpd.isTextMime(res.responseHeader.mime(), switchboard.mimeWhite))) {
|
|
// this is a file that is a possible candidate for parsing by the indexer
|
|
if (transformer.isIdentityTransformer()) {
|
|
log.logDebug("create passthrough (parse candidate) for url " + url);
|
|
// no transformation, only passthrough
|
|
// this is especially the case if the bluelist is empty
|
|
// in that case, the content is not scraped here but later
|
|
hfos = respond;
|
|
} else {
|
|
// make a scraper and transformer
|
|
log.logDebug("create scraper for url " + url);
|
|
scraper = new htmlFilterContentScraper(url);
|
|
hfos = new htmlFilterOutputStream(respond, scraper, transformer, (ext.length() == 0));
|
|
if (((htmlFilterOutputStream) hfos).binarySuspect()) {
|
|
scraper = null; // forget it, may be rubbish
|
|
log.logDebug("Content of " + url + " is probably binary. deleted scraper.");
|
|
}
|
|
cacheEntry.scraper = scraper;
|
|
}
|
|
} else {
|
|
log.logDebug("Resource " + url + " has wrong extension (" + ext + ") or wrong mime-type (" + res.responseHeader.mime() + "). not scraped");
|
|
scraper = null;
|
|
hfos = respond;
|
|
cacheEntry.scraper = scraper;
|
|
}
|
|
|
|
// handle incoming cookies
|
|
handleIncomingCookies(res.responseHeader, host, ip);
|
|
|
|
// request has been placed and result has been returned. work off response
|
|
try {
|
|
respondHeader(respond, res.status, res.responseHeader);
|
|
String storeError;
|
|
if ((storeError = cacheEntry.shallStoreCache()) == null) {
|
|
// we write a new cache entry
|
|
if ((contentLength > 0) && // known
|
|
(contentLength < 1048576)) {// 1 MB
|
|
// ok, we don't write actually into a file, only to RAM, and schedule writing the file.
|
|
byte[] cacheArray = res.writeContent(hfos);
|
|
log.logDebug("writeContent of " + url + " produced cacheArray = " + ((cacheArray == null) ? "null" : ("size=" + cacheArray.length)));
|
|
|
|
if (hfos instanceof htmlFilterOutputStream) ((htmlFilterOutputStream) hfos).finalize();
|
|
|
|
if (sizeBeforeDelete == -1) {
|
|
// totally fresh file
|
|
cacheEntry.status = plasmaHTCache.CACHE_FILL; // it's an insert
|
|
cacheManager.stackProcess(cacheEntry, cacheArray);
|
|
} else if (sizeBeforeDelete == cacheArray.length) {
|
|
// before we came here we deleted a cache entry
|
|
cacheArray = null;
|
|
cacheEntry.status = plasmaHTCache.CACHE_STALE_RELOAD_BAD;
|
|
cacheManager.stackProcess(cacheEntry); // unnecessary update
|
|
} else {
|
|
// before we came here we deleted a cache entry
|
|
cacheEntry.status = plasmaHTCache.CACHE_STALE_RELOAD_GOOD;
|
|
cacheManager.stackProcess(cacheEntry, cacheArray); // necessary update, write response header to cache
|
|
}
|
|
} else {
|
|
// the file is too big to cache it in the ram, or the size is unknown
|
|
// write to file right here.
|
|
cacheFile.getParentFile().mkdirs();
|
|
res.writeContent(hfos, cacheFile);
|
|
if (hfos instanceof htmlFilterOutputStream) ((htmlFilterOutputStream) hfos).finalize();
|
|
log.logDebug("for write-file of " + url + ": contentLength = " + contentLength + ", sizeBeforeDelete = " + sizeBeforeDelete);
|
|
if (sizeBeforeDelete == -1) {
|
|
// totally fresh file
|
|
cacheEntry.status = plasmaHTCache.CACHE_FILL; // it's an insert
|
|
cacheManager.stackProcess(cacheEntry);
|
|
} else if (sizeBeforeDelete == cacheFile.length()) {
|
|
// before we came here we deleted a cache entry
|
|
cacheEntry.status = plasmaHTCache.CACHE_STALE_RELOAD_BAD;
|
|
cacheManager.stackProcess(cacheEntry); // unnecessary update
|
|
} else {
|
|
// before we came here we deleted a cache entry
|
|
cacheEntry.status = plasmaHTCache.CACHE_STALE_RELOAD_GOOD;
|
|
cacheManager.stackProcess(cacheEntry); // necessary update, write response header to cache
|
|
}
|
|
// beware! all these writings will not fill the cacheEntry.cacheArray
|
|
// that means they are not available for the indexer (except they are scraped before)
|
|
}
|
|
} else {
|
|
// no caching
|
|
log.logDebug(cacheFile.toString() + " not cached: " + storeError);
|
|
res.writeContent(hfos, null);
|
|
if (hfos instanceof htmlFilterOutputStream) ((htmlFilterOutputStream) hfos).finalize();
|
|
if (sizeBeforeDelete == -1) {
|
|
// no old file and no load. just data passing
|
|
cacheEntry.status = plasmaHTCache.CACHE_PASSING;
|
|
cacheManager.stackProcess(cacheEntry);
|
|
} else {
|
|
// before we came here we deleted a cache entry
|
|
cacheEntry.status = plasmaHTCache.CACHE_STALE_NO_RELOAD;
|
|
cacheManager.stackProcess(cacheEntry);
|
|
}
|
|
}
|
|
} catch (SocketException e) {
|
|
// this may happen if the client suddenly closes its connection
|
|
// maybe the user has stopped loading
|
|
// in that case, we are not responsible and just forget it
|
|
// but we clean the cache also, since it may be only partial
|
|
// and most possible corrupted
|
|
if (cacheFile.exists()) cacheFile.delete();
|
|
respondHeader(respond,"404 client unexpectedly closed connection", new httpHeader(null));
|
|
} catch (IOException e) {
|
|
// can have various reasons
|
|
if (cacheFile.exists()) cacheFile.delete();
|
|
if (e.getMessage().indexOf("Corrupt GZIP trailer") >= 0) {
|
|
// just do nothing, we leave it this way
|
|
log.logDebug("ignoring bad gzip trail for URL " + url + " (" + e.getMessage() + ")");
|
|
} else {
|
|
respondHeader(respond,"404 client unexpectedly closed connection", new httpHeader(null));
|
|
log.logDebug("IOError for URL " + url + " (" + e.getMessage() + ") - responded 404");
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
} catch (Exception e) {
|
|
// this may happen if the targeted host does not exist or anything with the
|
|
// remote server was wrong.
|
|
// in any case, sending a 404 is appropriate
|
|
try {
|
|
if ((e.toString().indexOf("unknown host")) > 0) {
|
|
respondHeader(respond,"404 unknown host", new httpHeader(null));
|
|
} else {
|
|
respondHeader(respond,"404 Not Found", new httpHeader(null));
|
|
respond.write(("Exception occurred:\r\n").getBytes());
|
|
respond.write((e.toString() + "\r\n").getBytes());
|
|
respond.write(("[TRACE: ").getBytes());
|
|
e.printStackTrace(new PrintStream(respond));
|
|
respond.write(("]\r\n").getBytes());
|
|
}
|
|
} catch (Exception ee) {}
|
|
} finally {
|
|
if (remote != null) httpc.returnInstance(remote);
|
|
}
|
|
respond.flush();
|
|
}
|
|
|
|
|
|
private void respondError(OutputStream respond, String origerror, int errorcase, String url) {
|
|
FileInputStream fis = null;
|
|
try {
|
|
// set rewrite values
|
|
serverObjects tp = new serverObjects();
|
|
tp.put("errormessage", errorcase);
|
|
tp.put("httperror", origerror);
|
|
tp.put("url", url);
|
|
|
|
// rewrite the file
|
|
File file = new File(htRootPath, "/proxymsg/error.html");
|
|
byte[] result;
|
|
ByteArrayOutputStream o = new ByteArrayOutputStream();
|
|
fis = new FileInputStream(file);
|
|
httpTemplate.writeTemplate(fis, o, tp, "-UNRESOLVED_PATTERN-".getBytes());
|
|
o.close();
|
|
result = o.toByteArray();
|
|
|
|
// return header
|
|
httpHeader header = new httpHeader();
|
|
header.put(httpHeader.DATE, httpc.dateString(httpc.nowDate()));
|
|
header.put(httpHeader.CONTENT_TYPE, "text/html");
|
|
header.put(httpHeader.CONTENT_LENGTH, "" + o.size());
|
|
header.put(httpHeader.PRAGMA, "no-cache");
|
|
|
|
// write the array to the client
|
|
respondHeader(respond, origerror, header);
|
|
serverFileUtils.write(result, respond);
|
|
respond.flush();
|
|
} catch (IOException e) {
|
|
} finally {
|
|
if (fis != null) try { fis.close(); } catch (Exception e) {}
|
|
}
|
|
}
|
|
|
|
public void doHead(Properties conProp, httpHeader requestHeader, OutputStream respond) throws IOException {
|
|
String method = conProp.getProperty("METHOD");
|
|
String host = conProp.getProperty("HOST");
|
|
String path = conProp.getProperty("PATH");
|
|
String args = conProp.getProperty("ARGS"); // may be null if no args were given
|
|
int port;
|
|
int pos;
|
|
if ((pos = host.indexOf(":")) < 0) {
|
|
port = 80;
|
|
} else {
|
|
port = Integer.parseInt(host.substring(pos + 1));
|
|
host = host.substring(0, pos);
|
|
}
|
|
|
|
// check the blacklist, inspired by [AS]: respond a 404 for all AGIS (all you get is shit) servers
|
|
String hostlow = host.toLowerCase();
|
|
if (blacklistedURL(hostlow, path)) {
|
|
try {
|
|
respondHeader(respond,"404 Not Found (AGIS)", new httpHeader(null));
|
|
respond.write(("404 (generated): URL '" + hostlow + "' blocked by yacy proxy (blacklisted)\r\n").getBytes());
|
|
respond.flush();
|
|
serverLog.logInfo("PROXY", "AGIS blocking of host '" + hostlow + "'"); // debug
|
|
return;
|
|
} catch (Exception ee) {}
|
|
}
|
|
|
|
// set another userAgent, if not yellowlisted
|
|
if (!(yellowList.contains(domain(hostlow)))) {
|
|
// change the User-Agent
|
|
requestHeader.put(httpHeader.USER_AGENT, userAgent);
|
|
}
|
|
|
|
// resolve yacy and yacyh domains
|
|
String yAddress = yacyCore.seedDB.resolveYacyAddress(host);
|
|
|
|
// re-calc the url path
|
|
String remotePath = (args == null) ? path : (path + "?" + args);
|
|
|
|
// attach possible yacy-sublevel-domain
|
|
if ((yAddress != null) && ((pos = yAddress.indexOf("/")) >= 0)) remotePath = yAddress.substring(pos) + remotePath;
|
|
|
|
httpc remote = null;
|
|
httpc.response res = null;
|
|
|
|
try {
|
|
// open the connection
|
|
if (yAddress == null) {
|
|
remote = newhttpc(host, port, timeout);
|
|
} else {
|
|
remote = newhttpc(yAddress, timeout); // with [AS] patch
|
|
}
|
|
res = remote.HEAD(remotePath, requestHeader);
|
|
respondHeader(respond, res.status, res.responseHeader);
|
|
} catch (Exception e) {
|
|
try {
|
|
respondHeader(respond,"404 Not Found", new httpHeader(null));
|
|
respond.write(("Exception occurred:\r\n").getBytes());
|
|
respond.write((e.toString() + "\r\n").getBytes());
|
|
respond.write(("[TRACE: ").getBytes());
|
|
e.printStackTrace(new PrintStream(respond));
|
|
respond.write(("]\r\n").getBytes());
|
|
} catch (Exception ee) {}
|
|
} finally {
|
|
if (remote != null) httpc.returnInstance(remote);
|
|
}
|
|
|
|
respond.flush();
|
|
}
|
|
|
|
public void doPost(Properties conProp, httpHeader requestHeader, OutputStream respond, PushbackInputStream body) throws IOException {
|
|
String host = conProp.getProperty("HOST");
|
|
String path = conProp.getProperty("PATH");
|
|
String args = conProp.getProperty("ARGS"); // may be null if no args were given
|
|
int port;
|
|
int pos;
|
|
if ((pos = host.indexOf(":")) < 0) {
|
|
port = 80;
|
|
} else {
|
|
port = Integer.parseInt(host.substring(pos + 1));
|
|
host = host.substring(0, pos);
|
|
}
|
|
|
|
// set another userAgent, if not yellowlisted
|
|
if (!(yellowList.contains(domain(host).toLowerCase()))) {
|
|
// change the User-Agent
|
|
requestHeader.put(httpHeader.USER_AGENT, userAgent);
|
|
}
|
|
|
|
// resolve yacy and yacyh domains
|
|
String yAddress = yacyCore.seedDB.resolveYacyAddress(host);
|
|
|
|
// re-calc the url path
|
|
String remotePath = (args == null) ? path : (path + "?" + args);
|
|
|
|
// attach possible yacy-sublevel-domain
|
|
if ((yAddress != null) && ((pos = yAddress.indexOf("/")) >= 0)) remotePath = yAddress.substring(pos) + remotePath;
|
|
|
|
httpc remote = null;
|
|
httpc.response res = null;
|
|
|
|
try {
|
|
if (yAddress == null) {
|
|
remote = newhttpc(host, port, timeout);
|
|
} else {
|
|
remote = newhttpc(yAddress, timeout);
|
|
}
|
|
res = remote.POST(remotePath, requestHeader, body);
|
|
respondHeader(respond, res.status, res.responseHeader);
|
|
res.writeContent(respond, null);
|
|
remote.close();
|
|
} catch (Exception e) {
|
|
try {
|
|
respondHeader(respond,"404 Not Found", new httpHeader(null));
|
|
respond.write(("Exception occurred:\r\n").getBytes());
|
|
respond.write((e.toString() + "\r\n").getBytes());
|
|
respond.write(("[TRACE: ").getBytes());
|
|
e.printStackTrace(new PrintStream(respond));
|
|
respond.write(("]\r\n").getBytes());
|
|
} catch (Exception ee) {}
|
|
} finally {
|
|
if (remote != null) httpc.returnInstance(remote);
|
|
}
|
|
respond.flush();
|
|
}
|
|
|
|
public void doConnect(Properties conProp, de.anomic.http.httpHeader requestHeader, InputStream clientIn, OutputStream clientOut) throws IOException {
|
|
String host = conProp.getProperty("HOST");
|
|
int port = Integer.parseInt(conProp.getProperty("PORT"));
|
|
String httpVersion = conProp.getProperty("HTTP");
|
|
int timeout = Integer.parseInt(switchboard.getConfig("clientTimeout", "10000"));
|
|
|
|
// possibly branch into PROXY-PROXY connection
|
|
if (remoteProxyUse) {
|
|
httpc remoteProxy = null;
|
|
try {
|
|
remoteProxy = httpc.getInstance(host, port, timeout, false, remoteProxyHost, remoteProxyPort);
|
|
httpc.response response = remoteProxy.CONNECT(host, port, requestHeader);
|
|
response.print();
|
|
if (response.success()) {
|
|
// replace connection details
|
|
host = remoteProxyHost;
|
|
port = remoteProxyPort;
|
|
// go on (see below)
|
|
} else {
|
|
// pass error response back to client
|
|
respondHeader(clientOut, response.status, response.responseHeader);
|
|
return;
|
|
}
|
|
} catch (Exception e) {
|
|
throw new IOException(e.getMessage());
|
|
} finally {
|
|
if (remoteProxy != null) httpc.returnInstance(remoteProxy);
|
|
}
|
|
}
|
|
|
|
// try to establish connection to remote host
|
|
Socket sslSocket = new Socket(host, port);
|
|
sslSocket.setSoTimeout(timeout); // waiting time for write
|
|
sslSocket.setSoLinger(true, timeout); // waiting time for read
|
|
InputStream promiscuousIn = sslSocket.getInputStream();
|
|
OutputStream promiscuousOut = sslSocket.getOutputStream();
|
|
|
|
// now then we can return a success message
|
|
clientOut.write((httpVersion + " 200 Connection established" + serverCore.crlfString +
|
|
"Proxy-agent: YACY" + serverCore.crlfString +
|
|
serverCore.crlfString).getBytes());
|
|
|
|
log.logInfo("SSL CONNECTION TO " + host + ":" + port + " ESTABLISHED");
|
|
|
|
// start stream passing with mediate processes
|
|
try {
|
|
Mediate cs = new Mediate(sslSocket, clientIn, promiscuousOut);
|
|
Mediate sc = new Mediate(sslSocket, promiscuousIn, clientOut);
|
|
cs.start();
|
|
sc.start();
|
|
while ((sslSocket != null) &&
|
|
(sslSocket.isBound()) &&
|
|
(!(sslSocket.isClosed())) &&
|
|
(sslSocket.isConnected()) &&
|
|
((cs.isAlive()) || (sc.isAlive()))) {
|
|
// idle
|
|
try {Thread.currentThread().sleep(1000);} catch (InterruptedException e) {} // wait a while
|
|
}
|
|
// set stop mode
|
|
cs.pleaseTerminate();
|
|
sc.pleaseTerminate();
|
|
// wake up thread
|
|
cs.interrupt();
|
|
sc.interrupt();
|
|
// ...hope they have terminated...
|
|
} catch (IOException e) {
|
|
//System.out.println("promiscuous termination: " + e.getMessage());
|
|
}
|
|
|
|
}
|
|
|
|
public class Mediate extends Thread {
|
|
|
|
boolean terminate;
|
|
Socket socket;
|
|
InputStream in;
|
|
OutputStream out;
|
|
|
|
public Mediate(Socket socket, InputStream in, OutputStream out) throws IOException {
|
|
this.terminate = false;
|
|
this.in = in;
|
|
this.out = out;
|
|
this.socket = socket;
|
|
}
|
|
|
|
public void run() {
|
|
byte[] buffer = new byte[512];
|
|
int len;
|
|
try {
|
|
while ((socket != null) &&
|
|
(socket.isBound()) &&
|
|
(!(socket.isClosed())) &&
|
|
(socket.isConnected()) &&
|
|
(!(terminate)) &&
|
|
(in != null) &&
|
|
(out != null) &&
|
|
((len = in.read(buffer)) >= 0)
|
|
) {
|
|
out.write(buffer, 0, len);
|
|
}
|
|
} catch (IOException e) {}
|
|
}
|
|
|
|
public void pleaseTerminate() {
|
|
terminate = true;
|
|
}
|
|
}
|
|
|
|
private httpc newhttpc(String server, int port, int timeout) throws IOException {
|
|
// a new httpc connection, combined with possible remote proxy
|
|
boolean useProxy = remoteProxyUse;
|
|
// check no-proxy rule
|
|
if ((useProxy) && (!(remoteProxyAllowProxySet.contains(server)))) {
|
|
if (remoteProxyDisallowProxySet.contains(server)) {
|
|
useProxy = false;
|
|
} else {
|
|
// analyse remoteProxyNoProxy;
|
|
// set either remoteProxyAllowProxySet or remoteProxyDisallowProxySet accordingly
|
|
int i = 0;
|
|
while (i < remoteProxyNoProxyPatterns.length) {
|
|
if (server.matches(remoteProxyNoProxyPatterns[i])) {
|
|
// disallow proxy for this server
|
|
remoteProxyDisallowProxySet.add(server);
|
|
useProxy = false;
|
|
break;
|
|
}
|
|
i++;
|
|
}
|
|
if (i == remoteProxyNoProxyPatterns.length) {
|
|
// no pattern matches: allow server
|
|
remoteProxyAllowProxySet.add(server);
|
|
}
|
|
}
|
|
}
|
|
// branch to server/proxy
|
|
if (useProxy) {
|
|
return httpc.getInstance(server, port, timeout, false, remoteProxyHost, remoteProxyPort);
|
|
} else {
|
|
return httpc.getInstance(server, port, timeout, false);
|
|
}
|
|
}
|
|
|
|
private httpc newhttpc(String address, int timeout) throws IOException {
|
|
// a new httpc connection for <host>:<port>/<path> syntax
|
|
// this is called when a '.yacy'-domain is used
|
|
int p = address.indexOf(":");
|
|
if (p < 0) return null;
|
|
String server = address.substring(0, p);
|
|
address = address.substring(p + 1);
|
|
// remove possible path elements (may occur for 'virtual' subdomains
|
|
p = address.indexOf("/");
|
|
if (p >= 0) address = address.substring(0, p); // cut it off
|
|
int port = Integer.parseInt(address);
|
|
// normal creation of httpc object
|
|
return newhttpc(server, port, timeout);
|
|
}
|
|
|
|
private void respondHeader(OutputStream respond, String status, httpHeader header) throws IOException, SocketException {
|
|
|
|
// prepare header
|
|
//header.put("Server", "AnomicHTTPD (www.anomic.de)");
|
|
if (!(header.containsKey(httpHeader.DATE))) header.put(httpHeader.DATE, httpc.dateString(httpc.nowDate()));
|
|
if (!(header.containsKey(httpHeader.CONTENT_TYPE))) header.put(httpHeader.CONTENT_TYPE, "text/html"); // fix this
|
|
|
|
StringBuffer headerStringBuffer = new StringBuffer(200);
|
|
|
|
// write status line
|
|
headerStringBuffer.append("HTTP/1.1 ").append(status).append("\r\n");
|
|
|
|
//System.out.println("HEADER: PROXY TO CLIENT = " + header.toString()); // DEBUG
|
|
|
|
// write header
|
|
Iterator i = header.keySet().iterator();
|
|
String key;
|
|
String value;
|
|
char tag;
|
|
int count;
|
|
//System.out.println("vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv");
|
|
while (i.hasNext()) {
|
|
key = (String) i.next();
|
|
tag = key.charAt(0);
|
|
if ((tag != '*') && (tag != '#')) { // '#' in key is reserved for proxy attributes as artificial header values
|
|
count = header.keyCount(key);
|
|
for (int j = 0; j < count; j++) {
|
|
headerStringBuffer.append(key).append(": ").append((String) header.getSingle(key, j)).append("\r\n");
|
|
}
|
|
//System.out.println("#" + key + ": " + value);
|
|
}
|
|
}
|
|
headerStringBuffer.append("\r\n");
|
|
|
|
// end header
|
|
respond.write(headerStringBuffer.toString().getBytes());
|
|
respond.flush();
|
|
}
|
|
|
|
|
|
private void textMessage(OutputStream out, String body) throws IOException {
|
|
out.write(("HTTP/1.1 200 OK\r\n").getBytes());
|
|
out.write((httpHeader.SERVER + ": AnomicHTTPD (www.anomic.de)\r\n").getBytes());
|
|
out.write((httpHeader.DATE + ": " + httpc.dateString(httpc.nowDate()) + "\r\n").getBytes());
|
|
out.write((httpHeader.CONTENT_TYPE + ": text/plain\r\n").getBytes());
|
|
out.write((httpHeader.CONTENT_LENGTH + ": " + body.length() +"\r\n").getBytes());
|
|
out.write(("\r\n").getBytes());
|
|
out.flush();
|
|
out.write(body.getBytes());
|
|
out.flush();
|
|
}
|
|
|
|
private void transferFile(OutputStream out, File f) throws IOException {
|
|
InputStream source = new FileInputStream(f);
|
|
byte[] buffer = new byte[4096];
|
|
int bytes_read;
|
|
while ((bytes_read = source.read(buffer)) > 0) out.write(buffer, 0, bytes_read);
|
|
out.flush();
|
|
source.close();
|
|
}
|
|
|
|
}
|
|
|
|
/*
|
|
proxy test:
|
|
|
|
http://www.chipchapin.com/WebTools/cookietest.php?
|
|
http://xlists.aza.org/moderator/cookietest/cookietest1.php
|
|
http://vancouver-webpages.com/proxy/cache-test.html
|
|
|
|
*/
|