helma/modules/helma/Http.js

833 lines
27 KiB
JavaScript

/*
* Helma License Notice
*
* The contents of this file are subject to the Helma License
* Version 2.0 (the "License"). You may not use this file except in
* compliance with the License. A copy of the License is available at
* http://adele.helma.org/download/helma/license.txt
*
* Copyright 1998-2006 Helma Software. All Rights Reserved.
*
* $RCSfile: Http.js,v $
* $Author$
* $Revision$
* $Date$
*/
/**
* @fileoverview Fields and methods of the helma.Http class.
* <br /><br />
* To use this optional module, its repository needs to be added to the
* application, for example by calling app.addRepository('modules/helma/Http.js')
*/
// take care of any dependencies
app.addRepository('modules/core/Date.js');
/**
* Define the global namespace if not existing
*/
if (!global.helma) {
global.helma = {};
}
/**
* Creates a new instance of helma.Http
* @class This class provides functionality to programatically issue
* an Http request based on java.net.HttpUrlConnection.
* By default the request will use method <code>GET</code>.
* @returns A newly created helma.Http instance
* @constructor
*/
helma.Http = function() {
var self = this;
var proxy = null;
var content = "";
var userAgent = "Helma Http Client";
var method = "GET";
var cookies = null;
var credentials = null;
var followRedirects = true;
var binaryMode = false;
var headers = {};
var timeout = {
"connect": 0,
"socket": 0
};
var maxResponseSize = null;
var maxTries = 5;
var currentTries = 0;
var responseHandler = function(connection, result) {
var input;
var stream;
try {
stream = connection.getInputStream();
} catch (error) {
stream = connection.getErrorStream();
}
if (stream) {
if (connection.getContentEncoding() === 'gzip') {
stream = new java.util.zip.GZIPInputStream(stream);
}
input = new java.io.BufferedInputStream(stream);
}
if (input) {
var body = new java.io.ByteArrayOutputStream();
var buf = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 1024);
var len;
var currentSize = 0;
while ((len = input.read(buf)) > -1) {
body.write(buf, 0, len);
currentSize += len;
if (maxResponseSize && currentSize > maxResponseSize) {
throw new Error("Maximum allowed response size is exceeded");
}
}
try {
input.close();
} catch (error) {
// safe to ignore
}
if (binaryMode) {
result.content = body.toByteArray();
} else {
result.content = result.charset ?
body.toString(result.charset) :
body.toString();
}
// adjust content length
if (result.content) {
result.length = result.content.length;
}
}
};
/** @private */
var setTimeout = function(type, value) {
var v = java.lang.System.getProperty("java.specification.version");
if (parseFloat(v, 10) >= 1.5) {
timeout[type] = value;
} else {
app.logger.warn("helma.Http: Timeouts can only be set with Java Runtime version >= 1.5");
}
return true;
}
/**
* Sets the proxy host and port for later use. The argument must
* be in <code>host:port</code> format (eg. "proxy.example.com:3128").
* @param {String} proxyString The proxy to use for this request
* @see #getProxy
*/
this.setProxy = function(proxyString) {
var idx = proxyString.indexOf(":");
var host = proxyString.substring(0, idx);
var port = proxyString.substring(idx+1);
if (java.lang.Class.forName("java.net.Proxy") != null) {
// construct a proxy instance
var socket = new java.net.InetSocketAddress(host, port);
proxy = new java.net.Proxy(java.net.Proxy.Type.HTTP, socket);
} else {
// the pre jdk1.5 way: set the system properties
var sys = java.lang.System.getProperties();
if (host) {
app.logger.warn("[Helma Http Client] WARNING: setting system http proxy to " + host + ":" + port);
sys.put("http.proxySet", "true");
sys.put("http.proxyHost", host);
sys.put("http.proxyPort", port);
}
}
return;
};
/**
* Returns the proxy in <code>host:port</code> format
* @return The proxy defined for this request
* @type String
* @see #setProxy
*/
this.getProxy = function() {
if (proxy != null) {
return proxy.address().getHostName() + ":" + proxy.address().getPort();
} else if (sys.get("http.proxySet") == "true") {
return sys.get("http.proxyHost") + ":" + sys.get("http.proxyPort");
} else {
return null;
}
};
/**
* Sets the credentials for basic http authentication
* @param {String} username The username
* @param {String} password The password
*/
this.setCredentials = function(username, password) {
var str = username + ":" + password;
credentials = str.enbase64();
return;
}
/**
* Sets the content to send to the remote server within this request.
* @param {String|Object} stringOrObject The content of the request, which
* can be either a string or an object. In the latter case all properties
* and their values are concatenated into a single string.
* If a property is an array, then for each value the propertyname and value pair is added.
* If the name of an array property ends with "_array" then the _array part is removed.
*/
this.setContent = function(stringOrObject) {
if (stringOrObject != null) {
if (stringOrObject.constructor == Object) {
res.push();
var value;
for (var key in stringOrObject) {
value = stringOrObject[key];
if (value instanceof Array) {
if (key.substring(key.length - 6) == "_array")
key = key.substring(0,key.length - 6);
for (var i = 0; i < value.length; i++) {
res.write(encodeURIComponent(key));
res.write("=");
res.write(encodeURIComponent(value[i]));
res.write("&");
}
} else {
res.write(encodeURIComponent(key));
res.write("=");
res.write(encodeURIComponent(value));
res.write("&");
}
}
content = res.pop();
content = content.substring(0, content.length-1);
} else {
content = stringOrObject.toString();
}
} else {
content = null;
}
return;
};
/**
* Sets the request method to use.
* @param {String} m The method to use (<code>GET</code>, <code>POST</code> ...)
* @see #getMethod
*/
this.setMethod = function(m) {
method = m;
return;
};
/**
* Returns the currently defined request method.
* @returns The method used
* @type String
* @see #setMethod
*/
this.getMethod = function() {
return method;
};
/**
* Sets a single HTTP request header field
* @param {String} name The name of the header field
* @param {String} value The value of the header field
* @see #getHeader
*/
this.setHeader = function(name, value) {
headers[name] = value;
return;
};
/**
* Returns the value of the request header field with the given name
* @param {String} name The name of the request header field
* @returns The value of the request header field
* @type String
* @see #setHeader
*/
this.getHeader = function(name) {
return headers[name];
};
/**
* Adds a cookie with the name and value passed as arguments
* to the list of cookies to send to the remote server.
* @param {String} name The name of the cookie
* @param {String} value The value of the cookie
* @see #getCookie
* @see #getCookies
*/
this.setCookie = function(name, value) {
if (name != null && value != null) {
// store the cookie in the cookies map
if (!cookies) {
cookies = {};
}
cookies[name] = new helma.Http.Cookie(name, value);
}
return;
};
/**
* Returns the value of the cookie with the given name
* @param {String} name The name of the cookie
* @returns The value of the cookie
* @type String
* @see #setCookie
*/
this.getCookie = function(name) {
return (cookies != null) ? cookies[name] : null;
};
/**
* Adds the cookies passed as argument to the list of cookies to send
* to the remote server.
* @param {Array} cookies An array containing objects with the properties
* "name" (the name of the cookie) and "value" (the value of the cookie) set.
*/
this.setCookies = function(cookies) {
if (cookies != null) {
for (var i=0; i<cookies.length; i++) {
this.setCookie(cookies[i].name, cookies[i].value);
}
}
return;
};
/**
* Returns all cookies set for this client
* @return An object containing all cookies, where the property
* name is the name of the cookie, and the value is the cookie value
* @see #setCookie
*/
this.getCookies = function() {
return cookies;
};
/**
* Sets the connection timeout to the amount of milliseconds
* passed as argument
* @param {Number} timeout The connection timeout in milliseconds
* @see #getTimeout
*/
this.setTimeout = function(timeout) {
setTimeout("connect", timeout);
return;
};
/**
* Sets the read timeout (the maximum time a request may take after
* the connection has been successfully established) to the amount of
* milliseconds passed as argument.
* @param {Number} timeout The read timeout in milliseconds
* @see #getReadTimeout
*/
this.setReadTimeout = function(timeout) {
setTimeout("socket", timeout);
return true;
};
/**
* Returns the connection timeout
* @returns The connection timeout in milliseconds
* @type Number
* @see #setTimeout
*/
this.getTimeout = function() {
return timeout.connect;
};
/**
* Returns the read timeout (the maximum time a request may take after
* the connection has been successfully established).
* @returns The read timeout in milliseconds
* @type Number
* @see #setReadTimeout
*/
this.getReadTimeout = function() {
return timeout.socket;
};
/**
* Enables or disables following redirects
* @param {Boolean} value If false this client won't follow redirects (the default is
* to follow them)
* @see #getFollowRedirects
*/
this.setFollowRedirects = function(value) {
followRedirects = value;
return;
};
/**
* Returns true if the client follows redirects
* @returns True if the client follows redirects, false otherwise.
* @see #setFollowRedirects
*/
this.getFollowRedirects = function() {
return followRedirects;
};
/**
* Sets the HTTP "User-Agent" header field to the string passed as argument
* @param {String} agent The string to use as value of the
* "User-Agent" header field (defaults to "Helma Http Client")
* @see #getUserAgent
*/
this.setUserAgent = function(agent) {
userAgent = agent;
return;
};
/**
* Returns the value of the HTTP "User-Agent" header field
* @returns The value of the field
* @type String
* @see #setUserAgent
*/
this.getUserAgent = function() {
return userAgent;
};
/**
* Switches content text encoding on or off. Depending on this
* the content received from the remote server will be either a
* string or a byte array.
* @param {Boolean} mode If true binary mode is activated
* @see #getBinaryMode
*/
this.setBinaryMode = function(mode) {
binaryMode = mode;
return;
};
/**
* Returns the currently defined binary mode of this client
* @returns The binary mode of this client
* @type Boolean
* @see #setBinaryMode
*/
this.getBinaryMode = function() {
return binaryMode;
};
/**
* Sets the max allowed size for the response stream
* @param {Integer} Size in Byte
*/
this.setMaxResponseSize = function(size) {
maxResponseSize = size;
return;
};
/**
* Returns the currently set max response size
* @returns The max responsesize
* @type Integer
* @see #setMaxResponseSize
*/
this.getMaxResponseSize = function() {
return maxResponseSize;
};
/**
* Overloads the default response handler.
* Use this do implement your own response handling, like storing the response directly to the harddisk
* The handler function gets two parameter, first is the java.net.URLConnection and second is the result object.
* Note that custom response handler functions should check the HTTP status code before reading
* the response. The status code for successful requests is 200. Response bodies for requests with
* status codes less than 400 can be read from the connection's input stream, while response bodies
* with 4xx or 5xx status codes must be read using the error stream.
* @param {function} Response handler function
*/
this.setResponseHandler = function(callback) {
responseHandler = callback;
return;
};
/**
* Get the response handler. This is the function used to read the HTTP response body.
* @returns The response handler function
*/
this.getResponseHandler = function() {
return responseHandler;
}
/**
* Executes a http request
* @param {String} url The url to request
* @param {Date|String} opt If this argument is a string, it is used
* as value for the "If-None-Match" request header field. If it is a
* Date instance it is used as "IfModifiedSince" condition for this request.
* @return A result object containing the following properties:
* <ul>
* <li><code>url</code>: (String) The Url of the request</li>
* <li><code>location</code>: (String) The value of the location header field</li>
* <li><code>code</code>: (Number) The HTTP response code</li>
* <li><code>message</code>: (String) An optional HTTP response message</li>
* <li><code>length</code>: (Number) The content length of the response</li>
* <li><code>type</code>: (String) The mimetype of the response</li>
* <li><code>charset</code>: (String) The character set of the response</li>
* <li><code>encoding</code>: (String) An optional encoding to use with the response</li>
* <li><code>lastModified</code>: (String) The value of the lastModified response header field</li>
* <li><code>eTag</code>: (String) The eTag as received from the remote server</li>
* <li><code>cookie</code>: (helma.Http.Cookie) An object containing the cookie parameters, if the remote
server has set the "Set-Cookie" header field</li>
* <li><code>headers</code>: (java.util.Map) A map object containing the headers, access them using get("headername")
* <li><code>content</code>: (String|ByteArray) The response received from the server. Can be either
a string or a byte array (see #setBinaryMode)</li>
* </ul>
*/
this.getUrl = function(url, opt) {
if (typeof url == "string") {
if (!(url = helma.Http.evalUrl(url)))
throw new Error("'" + url + "' is not a valid URL.");
} else if (!(url instanceof java.net.URL)) {
throw new Error("'" + url + "' is not a valid URL.");
}
var conn = proxy ? url.openConnection(proxy) : url.openConnection();
// Note: we must call setInstanceFollowRedirects() instead of
// static method setFollowRedirects(), as the latter will
// set the default value for all url connections, and will not work for
// url connections that have already been created.
conn.setInstanceFollowRedirects(followRedirects);
conn.setAllowUserInteraction(false);
conn.setRequestMethod(method);
conn.setRequestProperty("User-Agent", userAgent);
if (opt) {
if (opt instanceof Date)
conn.setIfModifiedSince(opt.getTime());
else if ((typeof opt == "string") && (opt.length > 0))
conn.setRequestProperty("If-None-Match", opt);
}
var userinfo;
if (userinfo = url.getUserInfo()) {
userinfo = userinfo.split(/:/, 2);
this.setCredentials(userinfo[0], userinfo[1]);
}
if (credentials != null) {
conn.setRequestProperty("Authorization", "Basic " + credentials);
}
// set timeouts
if (parseFloat(java.lang.System.getProperty("java.specification.version"), 10) >= 1.5) {
conn.setConnectTimeout(timeout.connect);
conn.setReadTimeout(timeout.socket);
}
// set header fields
for (var i in headers) {
conn.setRequestProperty(i, headers[i]);
}
// set cookies
if (cookies != null) {
var arr = [];
for (var i in cookies) {
arr[arr.length] = cookies[i].getFieldValue();
}
conn.setRequestProperty("Cookie", arr.join(";"));
}
// set content
if (content) {
conn.setRequestProperty("Content-Length", content.length);
conn.setDoOutput(true);
var out = new java.io.OutputStreamWriter(conn.getOutputStream());
out.write(content);
out.flush();
out.close();
}
var result = {
url: conn.getURL(),
location: conn.getHeaderField("location"),
code: conn.getResponseCode(),
message: conn.getResponseMessage(),
length: conn.getContentLength(),
type: conn.getContentType(),
encoding: conn.getContentEncoding(),
lastModified: null,
eTag: conn.getHeaderField("ETag"),
cookies: null,
headers: conn.getHeaderFields(),
content: null,
}
// java.net.URLConnection does not follow redirects from http to https
// See https://bugs.java.com/bugdatabase/view_bug.do?bug_id=4620571
if (followRedirects && [301, 303].contains(result.code) && result.location) {
currentTries += 1;
if (currentTries >= maxTries) throw new Error('Too many redirects');
return this.getUrl(result.location, opt);
}
// parse all "Set-Cookie" header fields into an array of
// helma.Http.Cookie instances
var setCookies = conn.getHeaderFields().get("Set-Cookie");
if (setCookies != null) {
var arr = [];
var cookie;
for (var i=0; i<setCookies.size(); i++) {
if ((cookie = helma.Http.Cookie.parse(setCookies.get(i))) != null) {
arr.push(cookie);
}
}
if (arr.length > 0) {
result.cookies = arr;
}
}
var lastmod = conn.getLastModified();
if (lastmod) {
result.lastModified = new Date(lastmod);
}
if (maxResponseSize && result.length > maxResponseSize) {
throw new Error("Maximum allowed response size is exceeded");
}
if (result.type && result.type.indexOf("charset=") != -1) {
var charset = result.type.substring(result.type.indexOf("charset=") + 8);
charset = charset.replace(/[;"]/g, '').trim();
result.charset = charset;
}
// invoke response handler
responseHandler(conn, result);
conn.disconnect();
return result;
}
/** @ignore */
this.toString = function() {
return "[Helma Http Client]";
};
for (var i in this)
this.dontEnum(i);
return this;
};
/**
* Evaluates the url passed as argument.
* @param {String} url The url or uri string to evaluate
* @returns If the argument is a valid url, this method returns
* a new instance of java.net.URL, otherwise it returns null.
* @type java.net.URL
*/
helma.Http.evalUrl = function(url) {
try {
return new java.net.URL(url);
} catch (err) {
return null;
}
};
/**
* Sets the global http proxy setting. If no proxy definition
* is passed to this method, any existing proxy setting is
* cleared. Internally this method sets the system properties
* <code>http.proxySet</code>, <code>http.proxyHost</code> and
* <code>http.proxyPort</code>. Keep in mind that this is valid for
* the whole Java Virtual Machine, therefor using this method
* can potentially influence other running Helma applications too!
* @param {String} proxyString A proxy definition in <code>host:port</code>
* format (eg. "proxy.example.com:3128");
* @member helma.Http
*/
helma.Http.setProxy = function(proxyString) {
var sys = java.lang.System.getProperties();
if (proxyString) {
var idx = proxyString.indexOf(":");
var host = proxyString.substring(0, idx);
var port = proxyString.substring(idx+1);
if (!port)
port = "3128";
else if (typeof port == "number")
port = port.toString();
app.logger.info("helma.Http.setProxy " + proxyString);
sys.put("http.proxySet", "true");
sys.put("http.proxyHost", host);
sys.put("http.proxyPort", port);
} else {
sys.put("http.proxySet", "false");
sys.put("http.proxyHost", "");
sys.put("http.prodyPort", "");
}
return;
};
/**
* Returns the proxy setting of the Java Virtual Machine
* the Helma application server is running in. If no
* proxy is set, this method returns boolean false.
* @returns The global proxy setting in <code>host:port</code>
* format (eg. "proxy.example.com:3128"), or boolean false.
* @type String|Boolean
* @member helma.Http
*/
helma.Http.getProxy = function() {
var sys = java.lang.System.getProperties();
if (sys.get("http.proxySet") == "true")
return sys.get("http.proxyHost") + ":" + sys.get("http.proxyPort");
return false;
};
/**
* Static helper method to check if a request issued agains a
* Helma application is authorized or not.
* @param {String} name The username to check req.username against
* @param {String} pwd The password to check req.password against
* @return True if the request is authorized, false otherwise. In
* the latter case the current response is reset and the response code
* is set to "401" ("Authentication required").
* @type Boolean
*/
helma.Http.isAuthorized = function(name, pwd) {
if (!req.username || !req.password ||
req.username != name || req.password != pwd) {
res.reset();
res.status = 401;
res.realm = "Helma Http Authorization";
res.write("Authorization required.");
return false;
} else {
return true;
}
};
/** @ignore */
helma.Http.toString = function() {
return "[helma.Http]";
};
/**
* Creates a new instance of helma.Http.Cookie
* @class Instances of this object represent a HTTP cookie
* @param {String} name The name of the cookie
* @param {String} value The value of the cookie
* @returns A newly created Cookie instance
* @constructor
*/
helma.Http.Cookie = function(name, value) {
/**
* The name of the Cookie
* @type String
*/
this.name = name;
/**
* The value of the Cookie
* @type String
*/
this.value = value;
/**
* An optional date defining the lifetime of this cookie
* @type Date
*/
this.expires = null;
/**
* An optional path where this cookie is valid
* @type String
*/
this.path = null;
/**
* An optional domain where this cookie is valid
* @type String
*/
this.domain = null;
return this;
}
/**
* An instance of java.text.SimpleDateFormat used for both parsing
* an "expires" string into a date and vice versa
* @type java.text.SimpleDateFormat
* @final
*/
helma.Http.Cookie.DATEFORMAT = new java.text.SimpleDateFormat("EEE, dd-MMM-yy HH:mm:ss z");
/**
* A regular expression used for parsing cookie strings
* @type RegExp
* @final
*/
helma.Http.Cookie.PATTERN = /([^=;]+)=?([^;]*)(?:;\s*|$)/g;
/**
* Parses the cookie string passed as argument into an instance of helma.Http
* @param {String} cookieStr The cookie string as received from the remote server
* @returns An instance of helma.Http.Cookie containing the cookie parameters
* @type helma.Http.Cookie
*/
helma.Http.Cookie.parse = function(cookieStr) {
if (cookieStr != null) {
var cookie = new helma.Http.Cookie;
var m = helma.Http.Cookie.PATTERN.exec(cookieStr);
if (m) {
cookie.name = m[1].trim();
cookie.value = m[2] ? m[2].trim() : "";
}
while ((m = helma.Http.Cookie.PATTERN.exec(cookieStr)) != null) {
var key = m[1].trim();
var value = m[2] ? m[2].trim() : "";
switch (key.toLowerCase()) {
case "expires":
// try to parse the expires date string into a date object
try {
cookie.expires = helma.Http.Cookie.DATEFORMAT.parse(value);
} catch (e) {
// ignore
}
break;
default:
cookie[key.toLowerCase()] = value;
break;
}
}
return cookie;
}
return null;
};
/**
* Returns this cookie in a format useable to set the HTTP header field "Cookie"
* @return This cookie formatted as HTTP header field value
* @type String
*/
helma.Http.Cookie.prototype.getFieldValue = function() {
return this.name + "=" + this.value;
};
/** @ignore */
helma.Http.Cookie.prototype.toString = function() {
return "[helma.Http.Cookie " + this.name + " " + this.value + "]";
};
helma.lib = "Http";
helma.dontEnum(helma.lib);
for (var i in helma[helma.lib])
helma[helma.lib].dontEnum(i);
for (var i in helma[helma.lib].prototype)
helma[helma.lib].prototype.dontEnum(i);
for (var i in helma[helma.lib].Cookie.prototype)
helma[helma.lib].Cookie.prototype.dontEnum(i);
delete helma.lib;