Commit dc2c44c2 authored by Alex Wenckus's avatar Alex Wenckus Committed by alex

Incorporated feedback from code review.

git-svn-id: http://svn.igniterealtime.org/svn/repos/wildfire/trunk@6687 b35dd754-fafc-0310-a699-88a17e54d16e
parent 82bccaca
...@@ -32,7 +32,10 @@ import java.io.IOException; ...@@ -32,7 +32,10 @@ import java.io.IOException;
import java.net.InetAddress; import java.net.InetAddress;
/** /**
* Handles requests to the HTTP Bind service. * Servlet which handles requests to the HTTP binding service. It determines if there is currently
* an {@link HttpSession} related to the connection or if one needs to be created and then passes
* it off to the {@link HttpBindManager} for processing of the client request and formulating of
* the response.
* *
* @author Alexander Wenckus * @author Alexander Wenckus
*/ */
...@@ -50,6 +53,8 @@ public class HttpBindServlet extends HttpServlet { ...@@ -50,6 +53,8 @@ public class HttpBindServlet extends HttpServlet {
} }
} }
private ThreadLocal<XMPPPacketReader> localReader = new ThreadLocal<XMPPPacketReader>();
public HttpBindServlet() { public HttpBindServlet() {
} }
...@@ -66,8 +71,7 @@ public class HttpBindServlet extends HttpServlet { ...@@ -66,8 +71,7 @@ public class HttpBindServlet extends HttpServlet {
sessionManager.stop(); sessionManager.stop();
} }
@Override @Override protected void doPost(HttpServletRequest request, HttpServletResponse response)
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException throws ServletException, IOException
{ {
if (isContinuation(request, response)) { if (isContinuation(request, response)) {
...@@ -196,7 +200,7 @@ public class HttpBindServlet extends HttpServlet { ...@@ -196,7 +200,7 @@ public class HttpBindServlet extends HttpServlet {
{ {
byte[] content; byte[] content;
try { try {
content = connection.getDeliverable().getBytes("utf-8"); content = connection.getResponse().getBytes("utf-8");
} }
catch (HttpBindTimeoutException e) { catch (HttpBindTimeoutException e) {
content = createEmptyBody().getBytes("utf-8"); content = createEmptyBody().getBytes("utf-8");
...@@ -235,9 +239,12 @@ public class HttpBindServlet extends HttpServlet { ...@@ -235,9 +239,12 @@ public class HttpBindServlet extends HttpServlet {
private Document createDocument(HttpServletRequest request) throws private Document createDocument(HttpServletRequest request) throws
DocumentException, IOException, XmlPullParserException { DocumentException, IOException, XmlPullParserException {
// Reader is associated with a new XMPPPacketReader // Reader is associated with a new XMPPPacketReader
XMPPPacketReader reader = new XMPPPacketReader(); XMPPPacketReader reader = localReader.get();
reader.setXPPFactory(factory); if (reader == null) {
reader = new XMPPPacketReader();
reader.setXPPFactory(factory);
localReader.set(reader);
}
return reader.read("utf-8", request.getInputStream()); return reader.read("utf-8", request.getInputStream());
} }
} }
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
* $Revision: $ * $Revision: $
* $Date: $ * $Date: $
* *
* Copyright (C) 2006 Jive Software. All rights reserved. * Copyright (C) 2007 Jive Software. All rights reserved.
* *
* This software is published under the terms of the GNU Public License (GPL), * This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution. * a copy of which is included in this distribution.
...@@ -11,17 +11,16 @@ ...@@ -11,17 +11,16 @@
package org.jivesoftware.wildfire.http; package org.jivesoftware.wildfire.http;
import org.jivesoftware.wildfire.Connection;
import org.mortbay.util.ajax.Continuation; import org.mortbay.util.ajax.Continuation;
/** /**
* A connection to a client. The client will wait on getDeliverable() until the server forwards a * Represents one HTTP connection with a client using the HTTP Binding service. The client will wait
* message to it or the wait time on the session timesout. * on {@link #getResponse()} until the server forwards a message to it or the wait time on the
* session timesout.
* *
* @author Alexander Wenckus * @author Alexander Wenckus
*/ */
public class HttpConnection { public class HttpConnection {
private Connection.CompressionPolicy compressionPolicy;
private long requestId; private long requestId;
private String body; private String body;
private HttpSession session; private HttpSession session;
...@@ -29,15 +28,17 @@ public class HttpConnection { ...@@ -29,15 +28,17 @@ public class HttpConnection {
private boolean isClosed; private boolean isClosed;
private boolean isSecure = false; private boolean isSecure = false;
/**
* Constructs an HTTP Connection.
*
* @param requestId the ID which uniquely identifies this request.
* @param isSecure true if this connection is using HTTPS
*/
public HttpConnection(long requestId, boolean isSecure) { public HttpConnection(long requestId, boolean isSecure) {
this.requestId = requestId; this.requestId = requestId;
this.isSecure = isSecure; this.isSecure = isSecure;
} }
public boolean validate() {
return false;
}
/** /**
* The connection should be closed without delivering a stanza to the requestor. * The connection should be closed without delivering a stanza to the requestor.
*/ */
...@@ -54,10 +55,21 @@ public class HttpConnection { ...@@ -54,10 +55,21 @@ public class HttpConnection {
} }
} }
/**
* Returns true if this connection has been closed, either a response was delivered to the
* client or the server closed the connection aburbtly.
*
* @return true if this connection has been closed.
*/
public boolean isClosed() { public boolean isClosed() {
return isClosed; return isClosed;
} }
/**
* Returns true if this connection is using HTTPS.
*
* @return true if this connection is using HTTPS.
*/
public boolean isSecure() { public boolean isSecure() {
return isSecure; return isSecure;
} }
...@@ -93,16 +105,16 @@ public class HttpConnection { ...@@ -93,16 +105,16 @@ public class HttpConnection {
/** /**
* A call that will cause a wait, or in the case of Jetty the thread to be freed, if there is no * A call that will cause a wait, or in the case of Jetty the thread to be freed, if there is no
* deliverable currently available. Once the deliverable becomes available it is returned. * deliverable currently available. Once the response becomes available, it is returned.
* *
* @return the deliverable to send to the client * @return the deliverable to send to the client
* @throws HttpBindTimeoutException to indicate that the maximum wait time requested by the * @throws HttpBindTimeoutException to indicate that the maximum wait time requested by the
* client has been surpassed and an empty response should be returned. * client has been surpassed and an empty response should be returned.
*/ */
public String getDeliverable() throws HttpBindTimeoutException { public String getResponse() throws HttpBindTimeoutException {
if (body == null && continuation != null) { if (body == null && continuation != null) {
try { try {
body = waitForDeliverable(); body = waitForResponse();
} }
catch (HttpBindTimeoutException e) { catch (HttpBindTimeoutException e) {
this.isClosed = true; this.isClosed = true;
...@@ -115,31 +127,11 @@ public class HttpConnection { ...@@ -115,31 +127,11 @@ public class HttpConnection {
return body; return body;
} }
private String waitForDeliverable() throws HttpBindTimeoutException { /**
if (continuation.suspend(session.getWait() * 1000)) { * Returns the ID which uniquely identifies this connection.
String deliverable = (String) continuation.getObject(); *
// This will occur when the hold attribute of a session has been exceded. * @return the ID which uniquely identifies this connection.
if (deliverable == null) { */
throw new HttpBindTimeoutException();
}
return deliverable;
}
throw new HttpBindTimeoutException("Request " + requestId + " exceded response time from " +
"server of " + session.getWait() + " seconds.");
}
public boolean isCompressed() {
return false;
}
public Connection.CompressionPolicy getCompressionPolicy() {
return compressionPolicy;
}
public void setCompressionPolicy(Connection.CompressionPolicy compressionPolicy) {
this.compressionPolicy = compressionPolicy;
}
public long getRequestId() { public long getRequestId() {
return requestId; return requestId;
} }
...@@ -165,4 +157,17 @@ public class HttpConnection { ...@@ -165,4 +157,17 @@ public class HttpConnection {
void setContinuation(Continuation continuation) { void setContinuation(Continuation continuation) {
this.continuation = continuation; this.continuation = continuation;
} }
private String waitForResponse() throws HttpBindTimeoutException {
if (continuation.suspend(session.getWait() * 1000)) {
String deliverable = (String) continuation.getObject();
// This will occur when the hold attribute of a session has been exceded.
if (deliverable == null) {
throw new HttpBindTimeoutException();
}
return deliverable;
}
throw new HttpBindTimeoutException("Request " + requestId + " exceded response time from " +
"server of " + session.getWait() + " seconds.");
}
} }
...@@ -32,10 +32,9 @@ import java.util.concurrent.CopyOnWriteArraySet; ...@@ -32,10 +32,9 @@ import java.util.concurrent.CopyOnWriteArraySet;
/** /**
* A session represents a serious of interactions with an XMPP client sending packets using the HTTP * A session represents a serious of interactions with an XMPP client sending packets using the HTTP
* Binding protocol specified in * Binding protocol specified in <a href="http://www.xmpp.org/extensions/xep-0124.html">XEP-0124</a>.
* <a href="http://www.xmpp.org/extensions/xep-0124.html">XEP-0124</a>. A session can have several * A session can have several client connections open simultaneously while awaiting packets bound
* client connections open simultaneously while awaiting packets bound for the client from the * for the client from the server.
* server.
* *
* @author Alexander Wenckus * @author Alexander Wenckus
*/ */
...@@ -63,119 +62,16 @@ public class HttpSession extends ClientSession { ...@@ -63,119 +62,16 @@ public class HttpSession extends ClientSession {
this.lastRequestID = rid; this.lastRequestID = rid;
} }
HttpConnection createConnection(long rid, boolean isPoll, boolean isSecure) /**
throws HttpConnectionClosedException, HttpBindException * Returns the stream features which are available for this session.
{ *
HttpConnection connection = new HttpConnection(rid, isSecure); * @return the stream features which are available for this session.
if(rid <= lastRequestID) { */
Deliverable deliverable = retrieveDeliverable(rid);
if (deliverable == null) {
Log.warn("Deliverable unavailable for " + rid);
throw new HttpBindException("Unexpected RID Error", true, 404);
}
connection.deliverBody(deliverable.getDeliverable());
return connection;
}
else if (rid > (lastRequestID + hold)) {
// TODO handle the case of greater RID which basically has it wait
Log.warn("Request " + rid + " > " + (lastRequestID + hold) + ", ending session.");
throw new HttpBindException("Unexpected RID Error", true, 404);
}
addConnection(connection, isPoll);
return connection;
}
private Deliverable retrieveDeliverable(long rid) throws HttpBindException {
for(Deliverable delivered : sentElements) {
if(delivered.getRequestID() == rid) {
return delivered;
}
}
return null;
}
private void addConnection(HttpConnection connection, boolean isPoll) throws HttpBindException,
HttpConnectionClosedException
{
if(connection == null) {
throw new IllegalArgumentException("Connection cannot be null.");
}
if(isPoll) {
checkPollingInterval();
}
if(isSecure && !connection.isSecure()) {
throw new HttpBindException("Session was started from secure connection, all " +
"connections on this session must be secured.", false, 403);
}
connection.setSession(this);
// We aren't supposed to hold connections open or we already have some packets waiting
// to be sent to the client.
if (hold <= 0 || pendingElements.size() > 0) {
String deliverable = createDeliverable(pendingElements);
try {
fireConnectionOpened(connection);
deliver(connection, deliverable);
fireConnectionClosed(connection);
pendingElements.clear();
}
catch (HttpConnectionClosedException he) {
throw he;
}
}
else {
// With this connection we need to check if we will have too many connections open,
// closing any extras.
while (connectionQueue.size() >= hold) {
HttpConnection toClose = connectionQueue.remove();
toClose.close();
fireConnectionClosed(toClose);
}
connectionQueue.offer(connection);
fireConnectionOpened(connection);
}
lastRequestID = connection.getRequestId();
}
private void deliver(HttpConnection connection, String deliverable)
throws HttpConnectionClosedException
{
connection.deliverBody(deliverable);
Deliverable delivered = new Deliverable(deliverable);
delivered.setRequestID(connection.getRequestId());
while(sentElements.size() > hold) {
sentElements.remove(0);
}
sentElements.add(delivered);
}
private void fireConnectionOpened(HttpConnection connection) {
lastActivity = System.currentTimeMillis();
for(SessionListener listener : listeners) {
listener.connectionOpened(this, connection);
}
}
private void checkPollingInterval() throws HttpBindException {
long time = System.currentTimeMillis();
if(((time - lastPoll) / 1000) < maxPollingInterval) {
throw new HttpBindException("Too frequent polling minimum interval is "
+ maxPollingInterval + ", current interval " + ((lastPoll - time) / 1000),
true, 403);
}
lastPoll = time;
}
public Collection<Element> getAvailableStreamFeaturesElements() { public Collection<Element> getAvailableStreamFeaturesElements() {
List<Element> elements = new ArrayList<Element>(); List<Element> elements = new ArrayList<Element>();
Element sasl = SASLAuthentication.getSASLMechanismsElement(this); Element sasl = SASLAuthentication.getSASLMechanismsElement(this);
if(sasl != null) { if (sasl != null) {
elements.add(sasl); elements.add(sasl);
} }
...@@ -201,103 +97,37 @@ public class HttpSession extends ClientSession { ...@@ -201,103 +97,37 @@ public class HttpSession extends ClientSession {
public String getAvailableStreamFeatures() { public String getAvailableStreamFeatures() {
StringBuilder sb = new StringBuilder(200); StringBuilder sb = new StringBuilder(200);
for(Element element : getAvailableStreamFeaturesElements()) { for (Element element : getAvailableStreamFeaturesElements()) {
sb.append(element.asXML()); sb.append(element.asXML());
} }
return sb.toString(); return sb.toString();
} }
/**
* Closes the session. After a session has been closed it will no longer accept new connections
* on the session ID.
*/
public synchronized void close() { public synchronized void close() {
conn.close(); if (isClosed) {
}
private synchronized void closeConnection() {
if(isClosed) {
return; return;
} }
isClosed = true; conn.close();
if(pendingElements.size() > 0) {
failDelivery();
}
while (connectionQueue.size() > 0) {
HttpConnection toClose = connectionQueue.remove();
toClose.close();
fireConnectionClosed(toClose);
}
for(SessionListener listener : listeners) {
listener.sessionClosed(this);
}
this.listeners.clear();
}
private void failDelivery() {
for(Deliverable deliverable : pendingElements) {
Packet packet = deliverable.packet;
if (packet != null && packet instanceof Message) {
XMPPServer.getInstance().getOfflineMessageStrategy()
.storeOffline((Message) packet);
}
}
pendingElements.clear();
} }
/**
* Returns true if this session has been closed and no longer activley accepting connections.
*
* @return true if this session has been closed and no longer activley accepting connections.
*/
public synchronized boolean isClosed() { public synchronized boolean isClosed() {
return isClosed; return isClosed;
} }
private synchronized void deliver(String text) {
deliver(new Deliverable(text));
}
private synchronized void deliver(Packet stanza) {
deliver(new Deliverable(stanza));
}
private void deliver(Deliverable stanza) {
String deliverable = createDeliverable(Arrays.asList(stanza));
boolean delivered = false;
while(!delivered && connectionQueue.size() > 0) {
HttpConnection connection = connectionQueue.remove();
try {
deliver(connection, deliverable);
delivered = true;
fireConnectionClosed(connection);
}
catch (HttpConnectionClosedException e) {
/* Connection was closed, try the next one */
}
}
if(!delivered) {
pendingElements.add(stanza);
}
}
private void fireConnectionClosed(HttpConnection connection) {
lastActivity = System.currentTimeMillis();
for(SessionListener listener : listeners) {
listener.connectionClosed(this, connection);
}
}
private String createDeliverable(Collection<Deliverable> elements) {
StringBuilder builder = new StringBuilder();
builder.append("<body xmlns='" + "http://jabber.org/protocol/httpbind" + "'>");
for(Deliverable child : elements) {
builder.append(child.getDeliverable());
}
builder.append("</body>");
return builder.toString();
}
/** /**
* Specifies the longest time (in seconds) that the connection manager is allowed * Specifies the longest time (in seconds) that the connection manager is allowed to wait before
* to wait before responding to any request during the session. This enables the client to * responding to any request during the session. This enables the client to prevent its TCP
* prevent its TCP connection from expiring due to inactivity, as well as to limit the delay * connection from expiring due to inactivity, as well as to limit the delay before it discovers
* before it discovers any network failure. * any network failure.
* *
* @param wait the longest time it is permissible to wait for a response. * @param wait the longest time it is permissible to wait for a response.
*/ */
...@@ -306,10 +136,10 @@ public class HttpSession extends ClientSession { ...@@ -306,10 +136,10 @@ public class HttpSession extends ClientSession {
} }
/** /**
* Specifies the longest time (in seconds) that the connection manager is allowed * Specifies the longest time (in seconds) that the connection manager is allowed to wait before
* to wait before responding to any request during the session. This enables the client to * responding to any request during the session. This enables the client to prevent its TCP
* prevent its TCP connection from expiring due to inactivity, as well as to limit the delay * connection from expiring due to inactivity, as well as to limit the delay before it discovers
* before it discovers any network failure. * any network failure.
* *
* @return the longest time it is permissible to wait for a response. * @return the longest time it is permissible to wait for a response.
*/ */
...@@ -318,23 +148,22 @@ public class HttpSession extends ClientSession { ...@@ -318,23 +148,22 @@ public class HttpSession extends ClientSession {
} }
/** /**
* Specifies the maximum number of requests the connection manager is allowed * Specifies the maximum number of requests the connection manager is allowed to keep waiting at
* to keep waiting at any one time during the session. (For example, if a constrained client * any one time during the session. (For example, if a constrained client is unable to keep open
* is unable to keep open more than two HTTP connections to the same HTTP server simultaneously, * more than two HTTP connections to the same HTTP server simultaneously, then it SHOULD specify
* then it SHOULD specify a value of "1".) * a value of "1".)
* *
* @param hold the maximum number of simultaneous waiting requests. * @param hold the maximum number of simultaneous waiting requests.
*
*/ */
public void setHold(int hold) { public void setHold(int hold) {
this.hold = hold; this.hold = hold;
} }
/** /**
* Specifies the maximum number of requests the connection manager is allowed * Specifies the maximum number of requests the connection manager is allowed to keep waiting at
* to keep waiting at any one time during the session. (For example, if a constrained client * any one time during the session. (For example, if a constrained client is unable to keep open
* is unable to keep open more than two HTTP connections to the same HTTP server simultaneously, * more than two HTTP connections to the same HTTP server simultaneously, then it SHOULD specify
* then it SHOULD specify a value of "1".) * a value of "1".)
* *
* @return the maximum number of simultaneous waiting requests * @return the maximum number of simultaneous waiting requests
*/ */
...@@ -342,10 +171,20 @@ public class HttpSession extends ClientSession { ...@@ -342,10 +171,20 @@ public class HttpSession extends ClientSession {
return hold; return hold;
} }
/**
* Sets the language this session is using.
*
* @param language the language this session is using.
*/
public void setLanaguage(String language) { public void setLanaguage(String language) {
this.language = language; this.language = language;
} }
/**
* Returns the language this session is using.
*
* @return the language this session is using.
*/
public String getLanguage() { public String getLanguage() {
return language; return language;
} }
...@@ -366,7 +205,7 @@ public class HttpSession extends ClientSession { ...@@ -366,7 +205,7 @@ public class HttpSession extends ClientSession {
* request occurs in the interval the session will be terminated. * request occurs in the interval the session will be terminated.
* *
* @return the max interval within which a client can send polling requests. If more than one * @return the max interval within which a client can send polling requests. If more than one
* request occurs in the interval the session will be terminated. * request occurs in the interval the session will be terminated.
*/ */
public int getMaxPollingInterval() { public int getMaxPollingInterval() {
return this.maxPollingInterval; return this.maxPollingInterval;
...@@ -375,71 +214,306 @@ public class HttpSession extends ClientSession { ...@@ -375,71 +214,306 @@ public class HttpSession extends ClientSession {
/** /**
* The max number of requests it is permissable for this session to have open at any one time. * The max number of requests it is permissable for this session to have open at any one time.
* *
* @param maxRequests The max number of requests it is permissable for this session to have * @param maxRequests The max number of requests it is permissable for this session to have open
* open at any one time. * at any one time.
*/ */
public void setMaxRequests(int maxRequests) { public void setMaxRequests(int maxRequests) {
this.maxRequests = maxRequests; this.maxRequests = maxRequests;
} }
/** /**
* Returns the max number of requests it is permissable for this session to have open at any * Returns the max number of requests it is permissable for this session to have open at any one
* one time. * time.
* *
* @return the max number of requests it is permissable for this session to have open at any * @return the max number of requests it is permissable for this session to have open at any one
* one time. * time.
*/ */
public int getMaxRequests() { public int getMaxRequests() {
return this.maxRequests; return this.maxRequests;
} }
/** /**
* Sets whether the initial request on the session was secure. * Returns true if all connections on this session should be secured, and false if they should
* not.
* *
* @param isSecure true if the initial request was secure and false if it wasn't. * @return true if all connections on this session should be secured, and false if they should
* not.
*/ */
protected void setSecure(boolean isSecure) { public boolean isSecure() {
this.isSecure = isSecure; return isSecure;
} }
/** /**
* Returns true if all connections on this session should be secured, and false if * Adds a {@link org.jivesoftware.wildfire.http.SessionListener} to this session. The listener
* they should not. * will be notified of changes to the session.
* *
* @return true if all connections on this session should be secured, and false if * @param listener the listener which is being added to the session.
* they should not.
*/ */
public boolean isSecure() {
return isSecure;
}
public void addSessionCloseListener(SessionListener listener) { public void addSessionCloseListener(SessionListener listener) {
listeners.add(listener); listeners.add(listener);
} }
/**
* Removes a {@link org.jivesoftware.wildfire.http.SessionListener} from this session. The
* listener will no longer be updated when an event occurs on the session.
*
* @param listener the session listener that is to be removed.
*/
public void removeSessionCloseListener(SessionListener listener) { public void removeSessionCloseListener(SessionListener listener) {
listeners.remove(listener); listeners.remove(listener);
} }
/**
* Sets the time, in seconds, after which this session will be considered inactive and be be
* terminated.
*
* @param inactivityTimeout the time, in seconds, after which this session will be considered
* inactive and be terminated.
*/
public void setInactivityTimeout(int inactivityTimeout) { public void setInactivityTimeout(int inactivityTimeout) {
this.inactivityTimeout = inactivityTimeout; this.inactivityTimeout = inactivityTimeout;
} }
/**
* Returns the time, in seconds, after which this session will be considered inactive and
* terminated.
*
* @return the time, in seconds, after which this session will be considered inactive and
* terminated.
*/
public int getInactivityTimeout() { public int getInactivityTimeout() {
return inactivityTimeout; return inactivityTimeout;
} }
/**
* Returns the number of connections currently awaiting a response on this session.
*
* @return the number of connections currently awaiting a response on this session.
*/
public int getConnectionCount() { public int getConnectionCount() {
return connectionQueue.size(); return connectionQueue.size();
} }
/**
* Returns the time in milliseconds since the epoch that this session was last active. Activity
* is a request was either made or responded to.
*
* @return the time in milliseconds since the epoch that this session was last active.
*/
public synchronized long getLastActivity() { public synchronized long getLastActivity() {
return lastActivity; return lastActivity;
} }
/** /**
* A virtual server connection relates to a http session which its self can relate to many * Sets whether the initial request on the session was secure.
* http connections. *
* @param isSecure true if the initial request was secure and false if it wasn't.
*/
protected void setSecure(boolean isSecure) {
this.isSecure = isSecure;
}
/**
* Creates a new connection on this session. If a response is currently available for this
* session the connection is responded to immediately, otherwise it is queued awaiting a
* response.
*
* @param rid the request id related to the connection.
* @param isPoll true if the request was a poll, no packets were sent along with the request.
* @param isSecure true if the connection was secured using HTTPS.
* @return the created {@link org.jivesoftware.wildfire.http.HttpConnection} which represents
* the connection.
* @throws HttpConnectionClosedException if the connection was closed before a response could
* be delivered.
* @throws HttpBindException if the connection has violated a facet of the HTTP binding
* protocol.
*/
HttpConnection createConnection(long rid, boolean isPoll, boolean isSecure)
throws HttpConnectionClosedException, HttpBindException {
HttpConnection connection = new HttpConnection(rid, isSecure);
if (rid <= lastRequestID) {
Deliverable deliverable = retrieveDeliverable(rid);
if (deliverable == null) {
Log.warn("Deliverable unavailable for " + rid);
throw new HttpBindException("Unexpected RID Error", true, 404);
}
connection.deliverBody(deliverable.getDeliverable());
return connection;
}
else if (rid > (lastRequestID + hold + 1)) {
// TODO handle the case of greater RID which basically has it wait
Log.warn("Request " + rid + " > " + (lastRequestID + hold + 1) + ", ending session.");
throw new HttpBindException("Unexpected RID Error", true, 404);
}
addConnection(connection, isPoll);
return connection;
}
private Deliverable retrieveDeliverable(long rid) throws HttpBindException {
for (Deliverable delivered : sentElements) {
if (delivered.getRequestID() == rid) {
return delivered;
}
}
return null;
}
private void addConnection(HttpConnection connection, boolean isPoll) throws HttpBindException,
HttpConnectionClosedException {
if (connection == null) {
throw new IllegalArgumentException("Connection cannot be null.");
}
if (isPoll) {
checkPollingInterval();
}
if (isSecure && !connection.isSecure()) {
throw new HttpBindException("Session was started from secure connection, all " +
"connections on this session must be secured.", false, 403);
}
connection.setSession(this);
// We aren't supposed to hold connections open or we already have some packets waiting
// to be sent to the client.
if (hold <= 0 || pendingElements.size() > 0) {
String deliverable = createDeliverable(pendingElements);
try {
fireConnectionOpened(connection);
deliver(connection, deliverable);
fireConnectionClosed(connection);
pendingElements.clear();
}
catch (HttpConnectionClosedException he) {
throw he;
}
}
else {
// With this connection we need to check if we will have too many connections open,
// closing any extras.
while (connectionQueue.size() >= hold) {
HttpConnection toClose = connectionQueue.remove();
toClose.close();
fireConnectionClosed(toClose);
}
connectionQueue.offer(connection);
fireConnectionOpened(connection);
}
lastRequestID = connection.getRequestId();
}
private void deliver(HttpConnection connection, String deliverable)
throws HttpConnectionClosedException {
connection.deliverBody(deliverable);
Deliverable delivered = new Deliverable(deliverable);
delivered.setRequestID(connection.getRequestId());
while (sentElements.size() > hold) {
sentElements.remove(0);
}
sentElements.add(delivered);
}
private void fireConnectionOpened(HttpConnection connection) {
lastActivity = System.currentTimeMillis();
for (SessionListener listener : listeners) {
listener.connectionOpened(this, connection);
}
}
private void checkPollingInterval() throws HttpBindException {
long time = System.currentTimeMillis();
if (((time - lastPoll) / 1000) < maxPollingInterval) {
throw new HttpBindException("Too frequent polling minimum interval is "
+ maxPollingInterval + ", current interval " + ((lastPoll - time) / 1000),
true, 403);
}
lastPoll = time;
}
private synchronized void deliver(String text) {
deliver(new Deliverable(text));
}
private synchronized void deliver(Packet stanza) {
deliver(new Deliverable(stanza));
}
private void deliver(Deliverable stanza) {
String deliverable = createDeliverable(Arrays.asList(stanza));
boolean delivered = false;
while (!delivered && connectionQueue.size() > 0) {
HttpConnection connection = connectionQueue.remove();
try {
deliver(connection, deliverable);
delivered = true;
fireConnectionClosed(connection);
}
catch (HttpConnectionClosedException e) {
/* Connection was closed, try the next one */
}
}
if (!delivered) {
pendingElements.add(stanza);
}
}
private void fireConnectionClosed(HttpConnection connection) {
lastActivity = System.currentTimeMillis();
for (SessionListener listener : listeners) {
listener.connectionClosed(this, connection);
}
}
private String createDeliverable(Collection<Deliverable> elements) {
StringBuilder builder = new StringBuilder();
builder.append("<body xmlns='" + "http://jabber.org/protocol/httpbind" + "'>");
for (Deliverable child : elements) {
builder.append(child.getDeliverable());
}
builder.append("</body>");
return builder.toString();
}
private synchronized void closeConnection() {
if (isClosed) {
return;
}
isClosed = true;
if (pendingElements.size() > 0) {
failDelivery();
}
while (connectionQueue.size() > 0) {
HttpConnection toClose = connectionQueue.remove();
toClose.close();
fireConnectionClosed(toClose);
}
for (SessionListener listener : listeners) {
listener.sessionClosed(this);
}
this.listeners.clear();
}
private void failDelivery() {
for (Deliverable deliverable : pendingElements) {
Packet packet = deliverable.packet;
if (packet != null && packet instanceof Message) {
XMPPServer.getInstance().getOfflineMessageStrategy()
.storeOffline((Message) packet);
}
}
pendingElements.clear();
}
/**
* A virtual server connection relates to a http session which its self can relate to many http
* connections.
*/ */
public static class HttpVirtualConnection extends VirtualConnection { public static class HttpVirtualConnection extends VirtualConnection {
...@@ -450,7 +524,7 @@ public class HttpSession extends ClientSession { ...@@ -450,7 +524,7 @@ public class HttpSession extends ClientSession {
} }
public void closeVirtualConnection() { public void closeVirtualConnection() {
((HttpSession)session).closeConnection(); ((HttpSession) session).closeConnection();
} }
public InetAddress getInetAddress() { public InetAddress getInetAddress() {
...@@ -458,15 +532,15 @@ public class HttpSession extends ClientSession { ...@@ -458,15 +532,15 @@ public class HttpSession extends ClientSession {
} }
public void systemShutdown() { public void systemShutdown() {
((HttpSession)session).closeConnection(); ((HttpSession) session).closeConnection();
} }
public void deliver(Packet packet) throws UnauthorizedException { public void deliver(Packet packet) throws UnauthorizedException {
((HttpSession)session).deliver(packet); ((HttpSession) session).deliver(packet);
} }
public void deliverRawText(String text) { public void deliverRawText(String text) {
((HttpSession)session).deliver(text); ((HttpSession) session).deliver(text);
} }
} }
...@@ -486,7 +560,7 @@ public class HttpSession extends ClientSession { ...@@ -486,7 +560,7 @@ public class HttpSession extends ClientSession {
} }
public String getDeliverable() { public String getDeliverable() {
if(text == null) { if (text == null) {
return packet.toXML(); return packet.toXML();
} }
else { else {
...@@ -506,4 +580,24 @@ public class HttpSession extends ClientSession { ...@@ -506,4 +580,24 @@ public class HttpSession extends ClientSession {
return (int) (o.getRequestID() - requestID); return (int) (o.getRequestID() - requestID);
} }
} }
/**
* A queue of all current connections. Connections are dealt with in order of their request
* IDs.
*/
private class ConnectionQueue {
private final HttpConnection[] connections;
private final String[] requestIds;
private int pointer = 0;
public ConnectionQueue(int size) {
this.connections = new HttpConnection[size];
this.requestIds = new String[size];
}
}
} }
...@@ -32,21 +32,41 @@ import java.net.InetAddress; ...@@ -32,21 +32,41 @@ import java.net.InetAddress;
* <a href="http://www.xmpp.org/extensions/xep-0124.html">XEP-0124</a>. * <a href="http://www.xmpp.org/extensions/xep-0124.html">XEP-0124</a>.
*/ */
public class HttpSessionManager { public class HttpSessionManager {
private SessionManager sessionManager; private SessionManager sessionManager;
private Map<String, HttpSession> sessionMap = new ConcurrentHashMap<String, HttpSession>(); private Map<String, HttpSession> sessionMap = new ConcurrentHashMap<String, HttpSession>();
private TimerTask inactivityTask; private TimerTask inactivityTask;
private SessionListener sessionListener = new SessionListener() {
public void connectionOpened(HttpSession session, HttpConnection connection) {
}
public void connectionClosed(HttpSession session, HttpConnection connection) {
}
public void sessionClosed(HttpSession session) {
sessionMap.remove(session.getStreamID().getID());
sessionManager.removeSession(session);
}
};
/**
* Creates a new HttpSessionManager instance.
*/
public HttpSessionManager() { public HttpSessionManager() {
this.sessionManager = SessionManager.getInstance(); this.sessionManager = SessionManager.getInstance();
} }
/**
* Starts the services used by the HttpSessionManager.
*/
public void start() { public void start() {
inactivityTask = new HttpSessionReaper(); inactivityTask = new HttpSessionReaper();
TaskEngine.getInstance().schedule(inactivityTask, 30 * JiveConstants.SECOND, TaskEngine.getInstance().schedule(inactivityTask, 30 * JiveConstants.SECOND,
30 * JiveConstants.SECOND); 30 * JiveConstants.SECOND);
} }
/**
* Stops any services and cleans up any resources used by the HttpSessionManager.
*/
public void stop() { public void stop() {
inactivityTask.cancel(); inactivityTask.cancel();
for(HttpSession session : sessionMap.values()) { for(HttpSession session : sessionMap.values()) {
...@@ -173,6 +193,47 @@ public class HttpSessionManager { ...@@ -173,6 +193,47 @@ public class HttpSessionManager {
return JiveGlobals.getIntProperty("xmpp.httpbind.client.idle", 30); return JiveGlobals.getIntProperty("xmpp.httpbind.client.idle", 30);
} }
/**
* Forwards a client request, which is related to a session, to the server. A connection is
* created and queued up in the provided session. When a connection reaches the top of a queue
* any pending packets bound for the client will be forwarded to the client through the
* connection.
*
* @param rid the unique, sequential, requestID sent from the client.
* @param session the HTTP session of the client that made the request.
* @param isSecure true if the request was made over a secure channel, HTTPS, and false if it
* was not.
* @param rootNode the XML body of the request.
* @return the created HTTP connection.
* @throws HttpBindException for several reasons: if the encoding inside of an auth packet is
* not recognized by the server, or if the packet type is not recognized.
* @throws HttpConnectionClosedException if the session is no longer available.
*/
public HttpConnection forwardRequest(long rid, HttpSession session, boolean isSecure,
Element rootNode) throws HttpBindException,
HttpConnectionClosedException
{
//noinspection unchecked
List<Element> elements = rootNode.elements();
boolean isPoll = elements.size() <= 0;
HttpConnection connection = session.createConnection(rid, isPoll, isSecure);
SessionPacketRouter router = new SessionPacketRouter(session);
for (Element packet : elements) {
try {
router.route(packet);
}
catch (UnsupportedEncodingException e) {
throw new HttpBindException("Bad auth request, unknown encoding", true, 400);
}
catch (UnknownStanzaException e) {
throw new HttpBindException("Unknown packet type.", false, 400);
}
}
return connection;
}
private HttpSession createSession(long rid, InetAddress address) throws UnauthorizedException { private HttpSession createSession(long rid, InetAddress address) throws UnauthorizedException {
// Create a ClientSession for this user. // Create a ClientSession for this user.
StreamID streamID = SessionManager.getInstance().nextStreamID(); StreamID streamID = SessionManager.getInstance().nextStreamID();
...@@ -180,18 +241,7 @@ public class HttpSessionManager { ...@@ -180,18 +241,7 @@ public class HttpSessionManager {
HttpSession session = sessionManager.createClientHttpSession(rid, address, streamID); HttpSession session = sessionManager.createClientHttpSession(rid, address, streamID);
// Register that the new session is associated with the specified stream ID // Register that the new session is associated with the specified stream ID
sessionMap.put(streamID.getID(), session); sessionMap.put(streamID.getID(), session);
session.addSessionCloseListener(new SessionListener() { session.addSessionCloseListener(sessionListener);
public void connectionOpened(HttpSession session, HttpConnection connection) {
}
public void connectionClosed(HttpSession session, HttpConnection connection) {
}
public void sessionClosed(HttpSession session) {
sessionMap.remove(session.getStreamID().getID());
sessionManager.removeSession(session);
}
});
return session; return session;
} }
...@@ -227,52 +277,12 @@ public class HttpSessionManager { ...@@ -227,52 +277,12 @@ public class HttpSessionManager {
return response.asXML(); return response.asXML();
} }
/**
* Forwards a client request, which is related to a session, to the server. A connection is
* created and queued up in the provided session. When a connection reaches the top of a queue
* any pending packets bound for the client will be forwarded to the client through the
* connection.
*
* @param rid the unique, sequential, requestID sent from the client.
* @param session the HTTP session of the client that made the request.
* @param isSecure true if the request was made over a secure channel, HTTPS, and false if it
* was not.
* @param rootNode the XML body of the request.
* @return the created HTTP connection.
* @throws HttpBindException for several reasons: if the encoding inside of an auth packet is
* not recognized by the server, or if the packet type is not recognized.
* @throws HttpConnectionClosedException if the session is no longer available.
*/
public HttpConnection forwardRequest(long rid, HttpSession session, boolean isSecure,
Element rootNode) throws HttpBindException,
HttpConnectionClosedException
{
//noinspection unchecked
List<Element> elements = rootNode.elements();
boolean isPoll = elements.size() <= 0;
HttpConnection connection = session.createConnection(rid, isPoll, isSecure);
SessionPacketRouter router = new SessionPacketRouter(session);
for (Element packet : elements) {
try {
router.route(packet);
}
catch (UnsupportedEncodingException e) {
throw new HttpBindException("Bad auth request, unknown encoding", true, 400);
}
catch (UnknownStanzaException e) {
throw new HttpBindException("Unknown packet type.", false, 400);
}
}
return connection;
}
private class HttpSessionReaper extends TimerTask { private class HttpSessionReaper extends TimerTask {
public void run() { public void run() {
long currentTime = System.currentTimeMillis();
for(HttpSession session : sessionMap.values()) { for(HttpSession session : sessionMap.values()) {
long lastActive = (System.currentTimeMillis() - session.getLastActivity()) / 1000; long lastActive = (currentTime - session.getLastActivity()) / 1000;
if (lastActive > session.getInactivityTimeout()) { if (lastActive > session.getInactivityTimeout()) {
session.close(); session.close();
} }
......
...@@ -24,9 +24,9 @@ import java.util.zip.GZIPOutputStream; ...@@ -24,9 +24,9 @@ import java.util.zip.GZIPOutputStream;
import java.io.*; import java.io.*;
/** /**
* * Combines and serves resources, such as javascript or css files.
*/ */
public class JavaScriptServlet extends HttpServlet { public class ResourceServlet extends HttpServlet {
// private static String suffix = ""; // Set to "_src" to use source version // private static String suffix = ""; // Set to "_src" to use source version
private static long expiresOffset = 3600 * 24 * 10; // 10 days util client cache expires private static long expiresOffset = 3600 * 24 * 10; // 10 days util client cache expires
private boolean debug = false; private boolean debug = false;
...@@ -44,10 +44,11 @@ public class JavaScriptServlet extends HttpServlet { ...@@ -44,10 +44,11 @@ public class JavaScriptServlet extends HttpServlet {
public void service(HttpServletRequest request, HttpServletResponse response) { public void service(HttpServletRequest request, HttpServletResponse response) {
boolean compress = false; boolean compress = false;
boolean javascript = request.getRequestURI().endsWith("scripts/");
if (!disableCompression) { if (!disableCompression) {
if (request.getHeader("accept-encoding") != null && if (request.getHeader("accept-encoding") != null &&
request.getHeader("accept-encoding").indexOf("gzip") != -1) request.getHeader("accept-encoding").indexOf("gzip") != -1) {
{
compress = true; compress = true;
} }
else if (request.getHeader("---------------") != null) { else if (request.getHeader("---------------") != null) {
...@@ -56,7 +57,12 @@ public class JavaScriptServlet extends HttpServlet { ...@@ -56,7 +57,12 @@ public class JavaScriptServlet extends HttpServlet {
} }
} }
response.setHeader("Content-type", "text/javascript"); if(javascript) {
response.setHeader("Content-type", "text/javascript");
}
else {
response.setHeader("Content-type", "text/css");
}
response.setHeader("Vary", "Accept-Encoding"); // Handle proxies response.setHeader("Vary", "Accept-Encoding"); // Handle proxies
if (!debug) { if (!debug) {
...@@ -74,21 +80,24 @@ public class JavaScriptServlet extends HttpServlet { ...@@ -74,21 +80,24 @@ public class JavaScriptServlet extends HttpServlet {
InputStream in = null; InputStream in = null;
try { try {
byte[] jsContent; byte[] content;
String cacheKey = String.valueOf(compress); String cacheKey = String.valueOf(compress + " " + javascript);
jsContent = cache.get(cacheKey); content = cache.get(cacheKey);
if (debug || jsContent == null) { if (javascript && (debug || content == null)) {
jsContent = getJavaScriptContent(compress); content = getJavaScriptContent(compress);
cache.put(cacheKey, jsContent); cache.put(cacheKey, content);
}
else if(!javascript && content == null) {
} }
response.setContentLength(jsContent.length); response.setContentLength(content.length);
if (compress) { if (compress) {
response.setHeader("Content-Encoding", "gzip"); response.setHeader("Content-Encoding", "gzip");
} }
// Write the content out // Write the content out
in = new ByteArrayInputStream(jsContent); in = new ByteArrayInputStream(content);
out = response.getOutputStream(); out = response.getOutputStream();
// Use a 128K buffer. // Use a 128K buffer.
...@@ -142,8 +151,9 @@ public class JavaScriptServlet extends HttpServlet { ...@@ -142,8 +151,9 @@ public class JavaScriptServlet extends HttpServlet {
} }
private static Collection<String> getJavascriptFiles() { private static Collection<String> getJavascriptFiles() {
return Arrays.asList("xmlextras.js", "connection.js", "dojo.js", return Arrays.asList("prototype.js", "getelementsbyselector.js", "sarissa.js",
"flash.js"); "connection.js", "yahoo-min.js", "dom-min.js", "event-min.js", "dragdrop-min.js",
"yui-ext.js", "spank.js");
} }
private static String getJavaScriptFile(String path) { private static String getJavaScriptFile(String path) {
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
<servlet> <servlet>
<servlet-name>JavaScriptServlet</servlet-name> <servlet-name>JavaScriptServlet</servlet-name>
<servlet-class>org.jivesoftware.wildfire.http.JavaScriptServlet</servlet-class> <servlet-class>org.jivesoftware.wildfire.http.ResourceServlet</servlet-class>
<load-on-startup>1</load-on-startup> <load-on-startup>1</load-on-startup>
</servlet> </servlet>
......
/*----------------------------------------------------------------------------\
| XML Extras |
|-----------------------------------------------------------------------------|
| Created by Erik Arvidsson |
| (http://webfx.eae.net/contact.html#erik) |
| For WebFX (http://webfx.eae.net/) |
|-----------------------------------------------------------------------------|
| XML and XML HTTP request abstraction. |
|-----------------------------------------------------------------------------|
| Copyright (c) 2001, 2002, 2003, 2006 Erik Arvidsson |
|-----------------------------------------------------------------------------|
| Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| use this file except in compliance with the License. You may obtain a copy |
| of the License at http://www.apache.org/licenses/LICENSE-2.0 |
| - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| License for the specific language governing permissions and limitations |
| under the License. |
|-----------------------------------------------------------------------------|
| 2001-09-27 | Original Version Posted. |
| 2006-05-29 | Changed license to Apache Software License 2.0. |
|-----------------------------------------------------------------------------|
| Created 2001-09-27 | All changes are in the log above. | Updated 2006-05-29 |
\----------------------------------------------------------------------------*/
//<script>
//////////////////
// Helper Stuff //
//////////////////
// used to find the Automation server name
function getDomDocumentPrefix() {
if (getDomDocumentPrefix.prefix)
return getDomDocumentPrefix.prefix;
var prefixes = ["MSXML2", "Microsoft", "MSXML", "MSXML3"];
var o;
for (var i = 0; i < prefixes.length; i++) {
try {
// try to create the objects
o = new ActiveXObject(prefixes[i] + ".DomDocument");
return getDomDocumentPrefix.prefix = prefixes[i];
}
catch (ex) {};
}
throw new Error("Could not find an installed XML parser");
}
function getXmlHttpPrefix() {
if (getXmlHttpPrefix.prefix)
return getXmlHttpPrefix.prefix;
var prefixes = ["MSXML2", "Microsoft", "MSXML", "MSXML3"];
var o;
for (var i = 0; i < prefixes.length; i++) {
try {
// try to create the objects
o = new ActiveXObject(prefixes[i] + ".XmlHttp");
return getXmlHttpPrefix.prefix = prefixes[i];
}
catch (ex) {};
}
throw new Error("Could not find an installed XML parser");
}
//////////////////////////
// Start the Real stuff //
//////////////////////////
// XmlHttp factory
function XmlHttp() {}
XmlHttp.create = function () {
try {
if (window.XMLHttpRequest) {
var req = new XMLHttpRequest();
// some versions of Moz do not support the readyState property
// and the onreadystate event so we patch it!
if (req.readyState == null) {
req.readyState = 1;
req.addEventListener("load", function () {
req.readyState = 4;
if (typeof req.onreadystatechange == "function")
req.onreadystatechange();
}, false);
}
return req;
}
if (window.ActiveXObject) {
return new ActiveXObject(getXmlHttpPrefix() + ".XmlHttp");
}
}
catch (ex) {}
// fell through
throw new Error("Your browser does not support XmlHttp objects");
};
// XmlDocument factory
function XmlDocument() {}
XmlDocument.create = function () {
try {
// DOM2
if (document.implementation && document.implementation.createDocument) {
var doc = document.implementation.createDocument("", "", null);
// some versions of Moz do not support the readyState property
// and the onreadystate event so we patch it!
if (doc.readyState == null) {
doc.readyState = 1;
doc.addEventListener("load", function () {
doc.readyState = 4;
if (typeof doc.onreadystatechange == "function")
doc.onreadystatechange();
}, false);
}
return doc;
}
if (window.ActiveXObject)
return new ActiveXObject(getDomDocumentPrefix() + ".DomDocument");
}
catch (ex) {}
throw new Error("Your browser does not support XmlDocument objects");
};
// Create the loadXML method and xml getter for Mozilla
if (window.DOMParser &&
window.XMLSerializer &&
window.Node && Node.prototype && Node.prototype.__defineGetter__) {
// XMLDocument did not extend the Document interface in some versions
// of Mozilla. Extend both!
//XMLDocument.prototype.loadXML =
Document.prototype.loadXML = function (s) {
// parse the string to a new doc
var doc2 = (new DOMParser()).parseFromString(s, "text/xml");
// remove all initial children
while (this.hasChildNodes())
this.removeChild(this.lastChild);
// insert and import nodes
for (var i = 0; i < doc2.childNodes.length; i++) {
this.appendChild(this.importNode(doc2.childNodes[i], true));
}
};
/*
* xml getter
*
* This serializes the DOM tree to an XML String
*
* Usage: var sXml = oNode.xml
*
*/
// XMLDocument did not extend the Document interface in some versions
// of Mozilla. Extend both!
/*
XMLDocument.prototype.__defineGetter__("xml", function () {
return (new XMLSerializer()).serializeToString(this);
});
*/
Document.prototype.__defineGetter__("xml", function () {
return (new XMLSerializer()).serializeToString(this);
});
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment