/*
* SslPollHandler.java
*
* Brazil project web application toolkit,
* export version: 2.3
* Copyright (c) 2008-2009 Sun Microsystems, Inc.
*
* Sun Public License Notice
*
* The contents of this file are subject to the Sun Public License Version
* 1.0 (the "License"). You may not use this file except in compliance with
* the License. A copy of the License is included as the file "license.terms",
* and also available at http://www.sun.com/
*
* The Original Code is from:
* Brazil project web application toolkit release 2.3.
* The Initial Developer of the Original Code is: suhler.
* Portions created by suhler are Copyright (C) Sun Microsystems, Inc.
* All Rights Reserved.
*
* Contributor(s): suhler.
*
* Version: 1.3
* Created by suhler on 08/03/05
* Last modified by suhler on 09/01/20 16:07:59
*
* Version Histories:
*
* 1.3 09/01/20-16:07:59 (suhler)
* doc updates
* .
*
* 1.2 08/03/06-09:29:45 (suhler)
* move to ssl package, code cleanup
*
* 1.2 70/01/01-00:00:02 (Codemgr)
* SunPro Code Manager data about conflicts, renames, etc...
* Name history : 1 0 handlers/SslPollHandler.java
*
* 1.1 08/03/05-16:14:40 (suhler)
* date and time created 08/03/05 16:14:40 by suhler
*
*/
package sunlabs.brazil.ssl;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.SimpleTimeZone;
import java.util.Date;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Properties;
import java.util.StringTokenizer;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.MalformedURLException;
import sunlabs.brazil.server.Handler;
import sunlabs.brazil.server.Request;
import sunlabs.brazil.server.Server;
import sunlabs.brazil.util.Format;
import sunlabs.brazil.util.http.HttpUtil;
import sunlabs.brazil.util.regexp.Regexp;
import sunlabs.brazil.session.SessionManager;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSession;
/**
* Handler for periodically polling another web site, whose
* results are (optionally) added to the server's properties.
* This also includes the ability to request URL's on a cron-like schedule.
*
* NOTE:
* This version uses the built-in HTTP. It requires Java 1.4 or newer,
* doesn't cache connections, and doesn't support per connection proxies.
* However, it does support SSL.
* See also {@link sunlabs.brazil.handler.PollHandler}. These should be
* combined by adding additional protocol support to HttpRequest.
*
* UPDATED NOTE:
* The built-in HTTP stack now supports SSL
* (see {@link sunlabs.brazil.util.http.HttpsRequest}),
* so this class is deprecated.
*
* The result of fetching the url is expected to be a text document in
* java Properties format.
*
* Properties:
*
* - url
- URL to fetch periodically.
* any ${...} constructs are evaluated at each poll, with
* the values in the server properties object. If the URL
* starts with "/", then the current server is used.
*
- post
- The "post" data, if any. ${...} are evaluates as per
*
url
above.
* - headers
- A list of white space delimited tokens that refer to
* additional HTTP headers that are added onto the polled
* request. For each token the server properties
*
[token].name
and [token].value
* define a new http header.
* - interval
- The interval (in seconds) to fetch the url. Defaults
* to 10 seconds. If match is specified, this is the
* interval used to check for a time/date match.
* At each "interval", the current time format is computed, based
* on "format", below. If the computed format has not
* changed since the previous poll, then no poll is done.
* The interval is recalculated after each poll.
*
- fast
- If set, don't wait "interval" before 1st poll.
*
- prepend
- The string to prepend to the properties.
* If not supplied no properties are loaded.
*
- namespace
- The namespace to use to store the properties to.
* If the
sessionTable
(see below)parameter is
* identical to the sessionTable
parameter of
* the SetTemplate
, then this specifies the
* namespace
parameter that may be used with
* the SetTemplate
"namespace" parameter to
* obtain the extracted data. Defaults to the "prepend" parameter.
* - match
- If specified, a regular expression that must match
* the current time for this URL to run. The format to match is
* specified by the "format" parameter, below.
* "EEE-dd-HH-mm" (eg: Thu-Dec, 14, 14:12 pm).
*
- format
- a {@link java.text.SimpleDateFormat date format specifier}
* to use for matching "match"
* patterns. Defaults to "EE-MM-dd-HH-mm".
*
- sessionTable
*
- The name of the SessionManager table to use for
* storing values. By default, properties are stored in
* server.props. The value should match the
sessionTable
* used by the {@link sunlabs.brazil.template.SetTemplate} to allow
* values obtained by this handler to be accessable from within
* templates.
*
* If the sessionTable
is set, the namespace
* value is used to name the table (e.g. the namespace
* specified by {@link sunlabs.brazil.template.SetTemplate}. If no
* namespace parameter is given, then prepend
is used
* as the namespace parameter.
*
*
* If prepend is specified, the following additional
* properties are
* created, and added to the properties with the specified prefix.
*
* - count.attempts
- The total number of polls attemped.
*
- count.errors
- The total number of poll failures.
*
- error.at
- The poll attempt # for the last failure.
*
- error.msg
- The message describing the last failure.
*
- error.time
- The timestamp of the last failure.
*
- timestamp
- The timestamp for the last successful poll.
*
*
* @author Stephen Uhler
* @version %V% SslPollHandler.java 1.3
*/
public class SslPollHandler extends Thread implements Handler {
public String url; // url to fetch
public String post; // post data to send
public int interval; // how often to fetch it (ms)
long pollCount = 0; // # of times polled
long errorCount = 0; // # of times polls failed
Server server; // our server.
String prefix; // our prefix
String prepend; // our prefix in the props table
String namespace; // our namespace in the session manager
Regexp re; // the re to match the date/time expression
String then; // Last time we processed this url
String sessionTable; // the "other" session key
SimpleDateFormat date; // date format for "cron" matching
String tokens; // our tokens in server.props
String match;
boolean fast; // make first poll withoug waiting
HostnameVerifier verify = null; // our hostname verifier
/**
* Set up the initial configuration, and kick off a thread
* to periodically fetch the url.
*/
public boolean
init(Server server, String prefix) {
this.server = server;
this.prefix = prefix;
prepend = server.props.getProperty(prefix + "prepend");
namespace = server.props.getProperty(prefix + "namespace", prepend);
url = server.props.getProperty(prefix + "url");
post = server.props.getProperty(prefix + "post");
tokens = server.props.getProperty(prefix + "headers");
fast= (server.props.getProperty(prefix + "fast") != null);
sessionTable = server.props.getProperty(prefix + "sessionTable");
server.log(Server.LOG_DIAGNOSTIC, prefix, "Polling: " + url);
String format = server.props.getProperty(prefix + "format",
"EE-MM-dd-HH-mm");
date = new SimpleDateFormat(format, Locale.US);
date.setTimeZone(SimpleTimeZone.getDefault());
if (url == null) {
server.log(Server.LOG_WARNING, prefix, "Invalid url");
return false;
}
/*
* work on the match stuff
*/
match = server.props.getProperty(prefix + "match");
re = null;
if (match != null) {
try {
re = new Regexp(match, true);
} catch (Exception e) {
server.log(Server.LOG_WARNING, prefix, "Bad expression:" + e);
}
}
Thread poller = (Thread) this;
poller.setDaemon(true);
poller.start();
return true;
}
/**
* This might allow control over the polling via requests at a later date.
* For now, it always returns false.
*/
public boolean
respond(Request request) {
return false;
}
/**
* Periodically poll the url, and copy the results into the server
* properties.
*/
public void
run() {
Properties props;
if (sessionTable != null) {
props = (Properties) SessionManager.getSession(namespace,
sessionTable, Properties.class);
props.list(System.out);
} else {
props = new Properties();
}
boolean first = fast;
while(true) {
interval=10;
try {
String str = server.props.getProperty(prefix + "interval");
interval = Integer.decode(str).intValue();
} catch (Exception e) {}
if (interval < 1) {
interval=10;
}
server.log(Server.LOG_DIAGNOSTIC, prefix, "Sleeping: " +
interval);
try {
Thread.sleep(first ? 2 : interval * 1000);
} catch (InterruptedException e) {
break;
}
first = false;
if (re != null) {
String now = date.format(new Date(System.currentTimeMillis()));
server.log(Server.LOG_DIAGNOSTIC, prefix, "Checking: " +
now + " matches " + match);
if (now.equals(then)) {
server.log(Server.LOG_DIAGNOSTIC, prefix,
" Already polled at: " + now);
continue;
}
if (re.match(now) == null) {
server.log(Server.LOG_DIAGNOSTIC, prefix, now +
" match failed");
continue;
}
then = now;
}
url = Format.subst(server.props, url);
if (url.startsWith("/")) {
int port = server.listen.getLocalPort();
url = server.protocol + "://" + server.hostName +
(port != 80 ? ":" + port : "") + url;
}
server.log(Server.LOG_DIAGNOSTIC, prefix, "Polling: " + url);
pollCount++;
if (sessionTable == null) {
props.clear();
}
URL target = null;
HttpURLConnection con = null;
try {
target = new URL(url);
} catch (MalformedURLException e) {
server.log(Server.LOG_WARNING, prefix,
"Invalid url: " + e.getMessage());
return;
}
try {
con = (HttpURLConnection) target.openConnection();
} catch (IOException e) {
server.log(Server.LOG_WARNING, prefix,
"Invalid target " + target + ": " + e.getMessage());
return;
}
con.addRequestProperty("Via", "Brazil-Poll/" + Server.version);
con.setInstanceFollowRedirects(true);
con.setUseCaches(false);
if (con instanceof HttpsURLConnection) {
if (verify == null) {
verify = new anyHost(server, prefix);
}
((HttpsURLConnection)con).setHostnameVerifier(verify);
}
if (tokens != null) {
StringTokenizer st = new StringTokenizer(tokens);
while (st.hasMoreTokens()) {
String token = st.nextToken();
String name = server.props.getProperty(token + ".name");
String value = server.props.getProperty(token + ".value");
if (name!=null && value!=null) {
con.addRequestProperty(name, value);
}
}
}
if (con.getRequestProperty("host") == null) {
con.addRequestProperty("host", HttpUtil.extractUrlHost(url));
}
try {
if (post != null) {
con.setDoOutput(true);
con.setRequestMethod("POST");
OutputStream out = con.getOutputStream();
out.write(post.getBytes());
out.close();
} else {
con.setRequestMethod("GET");
}
server.log(Server.LOG_DIAGNOSTIC, prefix, "sending headers: " +
con.getRequestProperties());
con.connect();
server.log(Server.LOG_DIAGNOSTIC, prefix, "got headers: " +
con.getHeaderFields());
if (prepend != null) {
int code = con.getResponseCode();
if (code == 200) {
fillProps(props, con);
} else if (code > 299) {
throw new Exception("Request failed, code: " + code);
}
}
} catch (Exception e) {
server.log(Server.LOG_DIAGNOSTIC, prefix, "target error: " + e);
errorCount++;
if (prepend != null) {
props.put(prepend + "error.msg", e.getMessage());
props.put(prepend + "error.time",
"" + System.currentTimeMillis());
props.put(prepend + "error.at", "" + pollCount);
}
}
server.log(Server.LOG_DIAGNOSTIC, prefix, " found " + props.size() +
" items");
if (prepend != null) {
props.put("timestamp", "" + System.currentTimeMillis());
props.put("count.attempts", "" + pollCount);
props.put("count.errors", "" + errorCount);
}
/*
* Copy the properties into the server.
*/
if (prepend != null && sessionTable == null) {
Enumeration enumer = props.propertyNames();
while(enumer.hasMoreElements()) {
String key = (String) enumer.nextElement();
server.props.put(prepend + key, props.getProperty(key));
}
}
}
}
/**
* Fill the properties from the Url Connection
*/
public void
fillProps(Properties props, HttpURLConnection target) throws IOException {
InputStream in = target.getInputStream();
props.load(in);
in.close();
}
/**
* Verify all host names; log a warning for mismatches.
*/
static class anyHost implements HostnameVerifier {
Server server;
String prefix;
public anyHost(Server server, String prefix) {
this.server = server;
this.prefix = prefix;
}
/**
* Allow connection to any host, log a warning for a mis-match
*/
public boolean verify(String hostname, SSLSession session) {
boolean ok = hostname.equals(session.getPeerHost());
server.log(ok? Server.LOG_DIAGNOSTIC : Server.LOG_WARNING, prefix,
"Host match: " + hostname + " ?=? " + session.getPeerHost());
return true;
}
}
}