diff options
673 files changed, 114516 insertions, 0 deletions
diff --git a/Android.mk b/Android.mk new file mode 100644 index 0000000..b503e0e --- /dev/null +++ b/Android.mk @@ -0,0 +1,28 @@ +# Copyright 2011, The Android Open Source Project +# +# 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. + +LOCAL_PATH := $(call my-dir) + +################################################## +# Static library +################################################## +include $(CLEAR_VARS) + +LOCAL_MODULE := smackxmpp +LOCAL_MODULE_TAGS := optional +LOCAL_SDK_VERSION := 9 + +LOCAL_SRC_FILES := $(call all-java-files-under,src) + +include $(BUILD_STATIC_JAVA_LIBRARY) @@ -0,0 +1,7 @@ +The directory contains the build environment of smack for Android (located in asmack-master/) +and the generated source files (located in src/). + +To update to the latest smack source. Please run the following: + +./update.sh +m -j24 smackxmpp diff --git a/asmack-master/build.bash b/asmack-master/build.bash index 874f238..d291220 100755 --- a/asmack-master/build.bash +++ b/asmack-master/build.bash @@ -542,6 +542,16 @@ fetchall createVersionTag createbuildsrc patchsrc "patch" + +## +## BEGIN Modification for android platform build system +## +echo done with android modifications +exit +## +## END Modification for android platform build system +## + if $BUILD_JINGLE ; then patchsrc "jingle" JINGLE_ARGS="-Djingle=lib/jstun.jar" diff --git a/src/META-INF/services/com.kenai.jbosh.HTTPSender b/src/META-INF/services/com.kenai.jbosh.HTTPSender new file mode 100644 index 0000000..3608d8e --- /dev/null +++ b/src/META-INF/services/com.kenai.jbosh.HTTPSender @@ -0,0 +1 @@ +com.kenai.jbosh.ApacheHTTPSender diff --git a/src/com/kenai/jbosh/AbstractAttr.java b/src/com/kenai/jbosh/AbstractAttr.java new file mode 100644 index 0000000..0d6f84c --- /dev/null +++ b/src/com/kenai/jbosh/AbstractAttr.java @@ -0,0 +1,116 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + + package com.kenai.jbosh; + +/** + * Abstract base class for creating BOSH attribute classes. Concrete + * implementations of this class will naturally inherit the underlying + * type's behavior for {@code equals()}, {@code hashCode()}, + * {@code toString()}, and {@code compareTo()}, allowing for the easy + * creation of objects which extend existing trivial types. This was done + * to comply with the prefactoring rule declaring, "when you are being + * abstract, be abstract all the way". + * + * @param <T> type of the extension object + */ +abstract class AbstractAttr<T extends Comparable> + implements Comparable { + + /** + * Captured value. + */ + private final T value; + + /** + * Creates a new encapsulated object instance. + * + * @param aValue encapsulated getValue + */ + protected AbstractAttr(final T aValue) { + value = aValue; + } + + /** + * Gets the encapsulated data value. + * + * @return data value + */ + public final T getValue() { + return value; + } + + /////////////////////////////////////////////////////////////////////////// + // Object method overrides: + + /** + * {@inheritDoc} + * + * @param otherObj object to compare to + * @return true if the objects are equal, false otherwise + */ + @Override + public boolean equals(final Object otherObj) { + if (otherObj == null) { + return false; + } else if (otherObj instanceof AbstractAttr) { + AbstractAttr other = + (AbstractAttr) otherObj; + return value.equals(other.value); + } else { + return false; + } + } + + /** + * {@inheritDoc} + * + * @return hashCode of the encapsulated object + */ + @Override + public int hashCode() { + return value.hashCode(); + } + + /** + * {@inheritDoc} + * + * @return string representation of the encapsulated object + */ + @Override + public String toString() { + return value.toString(); + } + + /////////////////////////////////////////////////////////////////////////// + // Comparable interface: + + /** + * {@inheritDoc} + * + * @param otherObj object to compare to + * @return -1, 0, or 1 + */ + @SuppressWarnings("unchecked") + public int compareTo(final Object otherObj) { + if (otherObj == null) { + return 1; + } else { + return value.compareTo(otherObj); + } + } + +} diff --git a/src/com/kenai/jbosh/AbstractBody.java b/src/com/kenai/jbosh/AbstractBody.java new file mode 100644 index 0000000..4d66c8c --- /dev/null +++ b/src/com/kenai/jbosh/AbstractBody.java @@ -0,0 +1,104 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +/** + * Class representing a single message to or from the BOSH connection + * manager (CM). + * <p/> + * These messages consist of a single {@code body} element + * (qualified within the BOSH namespace: + * {@code http://jabber.org/protocol/httpbind}) and contain zero or more + * child elements (of any namespace). These child elements constitute the + * message payload. + * <p/> + * In addition to the message payload, the attributes of the wrapper + * {@code body} element may also need to be used as part of the communication + * protocol being implemented on top of BOSH, or to define additional + * namespaces used by the child "payload" elements. These attributes are + * exposed via accessors. + */ +public abstract class AbstractBody { + + /////////////////////////////////////////////////////////////////////////// + // Constructor: + + /** + * Restrict subclasses to the local package. + */ + AbstractBody() { + // Empty + } + + /////////////////////////////////////////////////////////////////////////// + // Public methods: + + /** + * Get a set of all defined attribute names. + * + * @return set of qualified attribute names + */ + public final Set<BodyQName> getAttributeNames() { + Map<BodyQName, String> attrs = getAttributes(); + return Collections.unmodifiableSet(attrs.keySet()); + } + + /** + * Get the value of the specified attribute. + * + * @param attr name of the attribute to retriece + * @return attribute value, or {@code null} if not defined + */ + public final String getAttribute(final BodyQName attr) { + Map<BodyQName, String> attrs = getAttributes(); + return attrs.get(attr); + } + + /////////////////////////////////////////////////////////////////////////// + // Abstract methods: + + /** + * Get a map of all defined attribute names with their corresponding values. + * + * @return map of qualified attributes + */ + public abstract Map<BodyQName, String> getAttributes(); + + /** + * Get an XML String representation of this message. + * + * @return XML string representing the body message + */ + public abstract String toXML(); + + /////////////////////////////////////////////////////////////////////////// + // Package-private methods: + + /** + * Returns the qualified name of the root/wrapper element. + * + * @return qualified name + */ + static BodyQName getBodyQName() { + return BodyQName.createBOSH("body"); + } + +} diff --git a/src/com/kenai/jbosh/AbstractIntegerAttr.java b/src/com/kenai/jbosh/AbstractIntegerAttr.java new file mode 100644 index 0000000..1b827f9 --- /dev/null +++ b/src/com/kenai/jbosh/AbstractIntegerAttr.java @@ -0,0 +1,97 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +/** + * Abstract base class for attribute implementations based on {@code Integer} + * types. Additional support for parsing of integer values from their + * {@code String} representations as well as callback handling of value + * validity checks are also provided. + */ +abstract class AbstractIntegerAttr extends AbstractAttr<Integer> { + + /** + * Creates a new attribute object. + * + * @param val attribute value + * @throws BOSHException on parse or validation failure + */ + protected AbstractIntegerAttr(final int val) throws BOSHException { + super(Integer.valueOf(val)); + } + + /** + * Creates a new attribute object. + * + * @param val attribute value in string form + * @throws BOSHException on parse or validation failure + */ + protected AbstractIntegerAttr(final String val) throws BOSHException { + super(parseInt(val)); + } + + /** + * Utility method intended to be called by concrete implementation + * classes from within the {@code check()} method when the concrete + * class needs to ensure that the integer value does not drop below + * the specified minimum value. + * + * @param minVal minimum value to allow + * @throws BOSHException if the integer value is below the specific + * minimum + */ + protected final void checkMinValue(int minVal) throws BOSHException { + int intVal = getValue(); + if (intVal < minVal) { + throw(new BOSHException( + "Illegal attribute value '" + intVal + "' provided. " + + "Must be >= " + minVal)); + } + } + + /** + * Utility method to parse a {@code String} into an {@code Integer}, + * converting any possible {@code NumberFormatException} thrown into + * a {@code BOSHException}. + * + * @param str string to parse + * @return integer value + * @throws BOSHException on {@code NumberFormatException} + */ + private static int parseInt(final String str) throws BOSHException { + try { + return Integer.parseInt(str); + } catch (NumberFormatException nfx) { + throw(new BOSHException( + "Could not parse an integer from the value provided: " + + str, + nfx)); + } + } + + /** + * Returns the native {@code int} value of the underlying {@code Integer}. + * Will throw {@code NullPointerException} if the underlying + * integer was {@code null}. + * + * @return native {@code int} value + */ + public int intValue() { + return getValue().intValue(); + } + +} diff --git a/src/com/kenai/jbosh/ApacheHTTPResponse.java b/src/com/kenai/jbosh/ApacheHTTPResponse.java new file mode 100644 index 0000000..9f6731f --- /dev/null +++ b/src/com/kenai/jbosh/ApacheHTTPResponse.java @@ -0,0 +1,253 @@ +/* + * Copyright 2009 Guenther Niess + * + * 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. + */ + +package com.kenai.jbosh; + +import java.io.IOException; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ByteArrayEntity; + +import org.apache.http.protocol.BasicHttpContext; +import org.apache.http.protocol.HttpContext; +import org.apache.http.util.EntityUtils; + +final class ApacheHTTPResponse implements HTTPResponse { + + /////////////////////////////////////////////////////////////////////////// + // Constants: + + /** + * Name of the accept encoding header. + */ + private static final String ACCEPT_ENCODING = "Accept-Encoding"; + + /** + * Value to use for the ACCEPT_ENCODING header. + */ + private static final String ACCEPT_ENCODING_VAL = + ZLIBCodec.getID() + ", " + GZIPCodec.getID(); + + /** + * Name of the character set to encode the body to/from. + */ + private static final String CHARSET = "UTF-8"; + + /** + * Content type to use when transmitting the body data. + */ + private static final String CONTENT_TYPE = "text/xml; charset=utf-8"; + + /////////////////////////////////////////////////////////////////////////// + // Class variables: + + /** + * Lock used for internal synchronization. + */ + private final Lock lock = new ReentrantLock(); + + /** + * The execution state of an HTTP process. + */ + private final HttpContext context; + + /** + * HttpClient instance to use to communicate. + */ + private final HttpClient client; + + /** + * The HTTP POST request is sent to the server. + */ + private final HttpPost post; + + /** + * A flag which indicates if the transmission was already done. + */ + private boolean sent; + + /** + * Exception to throw when the response data is attempted to be accessed, + * or {@code null} if no exception should be thrown. + */ + private BOSHException toThrow; + + /** + * The response body which was received from the server or {@code null} + * if that has not yet happened. + */ + private AbstractBody body; + + /** + * The HTTP response status code. + */ + private int statusCode; + + /////////////////////////////////////////////////////////////////////////// + // Constructors: + + /** + * Create and send a new request to the upstream connection manager, + * providing deferred access to the results to be returned. + * + * @param client client instance to use when sending the request + * @param cfg client configuration + * @param params connection manager parameters from the session creation + * response, or {@code null} if the session has not yet been established + * @param request body of the client request + */ + ApacheHTTPResponse( + final HttpClient client, + final BOSHClientConfig cfg, + final CMSessionParams params, + final AbstractBody request) { + super(); + this.client = client; + this.context = new BasicHttpContext(); + this.post = new HttpPost(cfg.getURI().toString()); + this.sent = false; + + try { + String xml = request.toXML(); + byte[] data = xml.getBytes(CHARSET); + + String encoding = null; + if (cfg.isCompressionEnabled() && params != null) { + AttrAccept accept = params.getAccept(); + if (accept != null) { + if (accept.isAccepted(ZLIBCodec.getID())) { + encoding = ZLIBCodec.getID(); + data = ZLIBCodec.encode(data); + } else if (accept.isAccepted(GZIPCodec.getID())) { + encoding = GZIPCodec.getID(); + data = GZIPCodec.encode(data); + } + } + } + + ByteArrayEntity entity = new ByteArrayEntity(data); + entity.setContentType(CONTENT_TYPE); + if (encoding != null) { + entity.setContentEncoding(encoding); + } + post.setEntity(entity); + if (cfg.isCompressionEnabled()) { + post.setHeader(ACCEPT_ENCODING, ACCEPT_ENCODING_VAL); + } + } catch (Exception e) { + toThrow = new BOSHException("Could not generate request", e); + } + } + + /////////////////////////////////////////////////////////////////////////// + // HTTPResponse interface methods: + + /** + * Abort the client transmission and response processing. + */ + public void abort() { + if (post != null) { + post.abort(); + toThrow = new BOSHException("HTTP request aborted"); + } + } + + /** + * Wait for and then return the response body. + * + * @return body of the response + * @throws InterruptedException if interrupted while awaiting the response + * @throws BOSHException on communication failure + */ + public AbstractBody getBody() throws InterruptedException, BOSHException { + if (toThrow != null) { + throw(toThrow); + } + lock.lock(); + try { + if (!sent) { + awaitResponse(); + } + } finally { + lock.unlock(); + } + return body; + } + + /** + * Wait for and then return the response HTTP status code. + * + * @return HTTP status code of the response + * @throws InterruptedException if interrupted while awaiting the response + * @throws BOSHException on communication failure + */ + public int getHTTPStatus() throws InterruptedException, BOSHException { + if (toThrow != null) { + throw(toThrow); + } + lock.lock(); + try { + if (!sent) { + awaitResponse(); + } + } finally { + lock.unlock(); + } + return statusCode; + } + + /////////////////////////////////////////////////////////////////////////// + // Package-private methods: + + /** + * Await the response, storing the result in the instance variables of + * this class when they arrive. + * + * @throws InterruptedException if interrupted while awaiting the response + * @throws BOSHException on communication failure + */ + private synchronized void awaitResponse() throws BOSHException { + HttpEntity entity = null; + try { + HttpResponse httpResp = client.execute(post, context); + entity = httpResp.getEntity(); + byte[] data = EntityUtils.toByteArray(entity); + String encoding = entity.getContentEncoding() != null ? + entity.getContentEncoding().getValue() : + null; + if (ZLIBCodec.getID().equalsIgnoreCase(encoding)) { + data = ZLIBCodec.decode(data); + } else if (GZIPCodec.getID().equalsIgnoreCase(encoding)) { + data = GZIPCodec.decode(data); + } + body = StaticBody.fromString(new String(data, CHARSET)); + statusCode = httpResp.getStatusLine().getStatusCode(); + sent = true; + } catch (IOException iox) { + abort(); + toThrow = new BOSHException("Could not obtain response", iox); + throw(toThrow); + } catch (RuntimeException ex) { + abort(); + throw(ex); + } + } +} diff --git a/src/com/kenai/jbosh/ApacheHTTPSender.java b/src/com/kenai/jbosh/ApacheHTTPSender.java new file mode 100644 index 0000000..b3d3c93 --- /dev/null +++ b/src/com/kenai/jbosh/ApacheHTTPSender.java @@ -0,0 +1,156 @@ +/* + * Copyright 2009 Guenther Niess + * + * 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. + */ + +package com.kenai.jbosh; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.apache.http.HttpHost; +import org.apache.http.HttpVersion; +import org.apache.http.client.HttpClient; +import org.apache.http.conn.ClientConnectionManager; +import org.apache.http.conn.params.ConnManagerParams; +import org.apache.http.conn.params.ConnRoutePNames; +import org.apache.http.conn.scheme.PlainSocketFactory; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.conn.ssl.SSLSocketFactory; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpParams; +import org.apache.http.params.HttpProtocolParams; + +/** + * Implementation of the {@code HTTPSender} interface which uses the + * Apache HttpClient API to send messages to the connection manager. + */ +final class ApacheHTTPSender implements HTTPSender { + + /** + * Lock used for internal synchronization. + */ + private final Lock lock = new ReentrantLock(); + + /** + * Session configuration. + */ + private BOSHClientConfig cfg; + + /** + * HttpClient instance to use to communicate. + */ + private HttpClient httpClient; + + /////////////////////////////////////////////////////////////////////////// + // Constructors: + + /** + * Prevent construction apart from our package. + */ + ApacheHTTPSender() { + // Load Apache HTTP client class + HttpClient.class.getName(); + } + + /////////////////////////////////////////////////////////////////////////// + // HTTPSender interface methods: + + /** + * {@inheritDoc} + */ + public void init(final BOSHClientConfig session) { + lock.lock(); + try { + cfg = session; + httpClient = initHttpClient(session); + } finally { + lock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + public void destroy() { + lock.lock(); + try { + if (httpClient != null) { + httpClient.getConnectionManager().shutdown(); + } + } finally { + cfg = null; + httpClient = null; + lock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + public HTTPResponse send( + final CMSessionParams params, + final AbstractBody body) { + HttpClient mClient; + BOSHClientConfig mCfg; + lock.lock(); + try { + if (httpClient == null) { + httpClient = initHttpClient(cfg); + } + mClient = httpClient; + mCfg = cfg; + } finally { + lock.unlock(); + } + return new ApacheHTTPResponse(mClient, mCfg, params, body); + } + + /////////////////////////////////////////////////////////////////////////// + // Package-private methods: + + private synchronized HttpClient initHttpClient(final BOSHClientConfig config) { + // Create and initialize HTTP parameters + HttpParams params = new BasicHttpParams(); + ConnManagerParams.setMaxTotalConnections(params, 100); + HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1); + HttpProtocolParams.setUseExpectContinue(params, false); + if (config != null && + config.getProxyHost() != null && + config.getProxyPort() != 0) { + HttpHost proxy = new HttpHost( + config.getProxyHost(), + config.getProxyPort()); + params.setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy); + } + + // Create and initialize scheme registry + SchemeRegistry schemeRegistry = new SchemeRegistry(); + schemeRegistry.register( + new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)); + SSLSocketFactory sslFactory = SSLSocketFactory.getSocketFactory(); + sslFactory.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); + schemeRegistry.register( + new Scheme("https", sslFactory, 443)); + + // Create an HttpClient with the ThreadSafeClientConnManager. + // This connection manager must be used if more than one thread will + // be using the HttpClient. + ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry); + return new DefaultHttpClient(cm, params); + } +} diff --git a/src/com/kenai/jbosh/AttrAccept.java b/src/com/kenai/jbosh/AttrAccept.java new file mode 100644 index 0000000..4f767df --- /dev/null +++ b/src/com/kenai/jbosh/AttrAccept.java @@ -0,0 +1,74 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +/** + * Data type representing the getValue of the {@code accept} attribute of the + * {@code bosh} element. + */ +final class AttrAccept extends AbstractAttr<String> { + + /** + * Array of the accepted encodings. + */ + private final String[] encodings; + + /** + * Creates a new attribute object. + * + * @param val attribute getValue + * @throws BOSHException on parse or validation failure + */ + private AttrAccept(final String val) { + super(val); + encodings = val.split("[\\s,]+"); + } + + /** + * Creates a new attribute instance from the provided String. + * + * @param str string representation of the attribute + * @return attribute instance or {@code null} if provided string is + * {@code null} + * @throws BOSHException on parse or validation failure + */ + static AttrAccept createFromString(final String str) + throws BOSHException { + if (str == null) { + return null; + } else { + return new AttrAccept(str); + } + } + + /** + * Determines whether or not the specified encoding is supported. + * + * @param name encoding name + * @result {@code true} if the encoding is accepted, {@code false} + * otherwise + */ + boolean isAccepted(final String name) { + for (String str : encodings) { + if (str.equalsIgnoreCase(name)) { + return true; + } + } + return false; + } + +} diff --git a/src/com/kenai/jbosh/AttrAck.java b/src/com/kenai/jbosh/AttrAck.java new file mode 100644 index 0000000..6cfe22b --- /dev/null +++ b/src/com/kenai/jbosh/AttrAck.java @@ -0,0 +1,52 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +/** + * Data type representing the getValue of the {@code ack} attribute of the + * {@code bosh} element. + */ +final class AttrAck extends AbstractAttr<String> { + + /** + * Creates a new attribute object. + * + * @param val attribute getValue + * @throws BOSHException on parse or validation failure + */ + private AttrAck(final String val) throws BOSHException { + super(val); + } + + /** + * Creates a new attribute instance from the provided String. + * + * @param str string representation of the attribute + * @return attribute instance or {@code null} if provided string is + * {@code null} + * @throws BOSHException on parse or validation failure + */ + static AttrAck createFromString(final String str) + throws BOSHException { + if (str == null) { + return null; + } else { + return new AttrAck(str); + } + } + +} diff --git a/src/com/kenai/jbosh/AttrCharsets.java b/src/com/kenai/jbosh/AttrCharsets.java new file mode 100644 index 0000000..45ce78c --- /dev/null +++ b/src/com/kenai/jbosh/AttrCharsets.java @@ -0,0 +1,71 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +/** + * Data type representing the getValue of the {@code charsets} attribute of the + * {@code bosh} element. + */ +final class AttrCharsets extends AbstractAttr<String> { + + /** + * Array of the accepted character sets. + */ + private final String[] charsets; + + /** + * Creates a new attribute object. + * + * @param val attribute getValue + */ + private AttrCharsets(final String val) { + super(val); + charsets = val.split("\\ +"); + } + + /** + * Creates a new attribute instance from the provided String. + * + * @param str string representation of the attribute + * @return attribute instance or {@code null} if provided string is + * {@code null} + */ + static AttrCharsets createFromString(final String str) { + if (str == null) { + return null; + } else { + return new AttrCharsets(str); + } + } + + /** + * Determines whether or not the specified charset is supported. + * + * @param name encoding name + * @result {@code true} if the encoding is accepted, {@code false} + * otherwise + */ + boolean isAccepted(final String name) { + for (String str : charsets) { + if (str.equalsIgnoreCase(name)) { + return true; + } + } + return false; + } + +} diff --git a/src/com/kenai/jbosh/AttrHold.java b/src/com/kenai/jbosh/AttrHold.java new file mode 100644 index 0000000..56f21dd --- /dev/null +++ b/src/com/kenai/jbosh/AttrHold.java @@ -0,0 +1,53 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +/** + * Data type representing the getValue of the {@code hold} attribute of the + * {@code bosh} element. + */ +final class AttrHold extends AbstractIntegerAttr { + + /** + * Creates a new attribute object. + * + * @param val attribute getValue + * @throws BOSHException on parse or validation failure + */ + private AttrHold(final String val) throws BOSHException { + super(val); + checkMinValue(0); + } + + /** + * Creates a new attribute instance from the provided String. + * + * @param str string representation of the attribute + * @return attribute instance or {@code null} if provided string is + * {@code null} + * @throws BOSHException on parse or validation failure + */ + static AttrHold createFromString(final String str) + throws BOSHException { + if (str == null) { + return null; + } else { + return new AttrHold(str); + } + } + +} diff --git a/src/com/kenai/jbosh/AttrInactivity.java b/src/com/kenai/jbosh/AttrInactivity.java new file mode 100644 index 0000000..14ab7d4 --- /dev/null +++ b/src/com/kenai/jbosh/AttrInactivity.java @@ -0,0 +1,53 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +/** + * Data type representing the value of the {@code inactivity} attribute of the + * {@code bosh} element. + */ +final class AttrInactivity extends AbstractIntegerAttr { + + /** + * Creates a new attribute object. + * + * @param val attribute value + * @throws BOSHException on parse or validation failure + */ + private AttrInactivity(final String val) throws BOSHException { + super(val); + checkMinValue(0); + } + + /** + * Creates a new attribute instance from the provided String. + * + * @param str string representation of the attribute + * @return instance of the attribute for the specified string, or + * {@code null} if input string is {@code null} + * @throws BOSHException on parse or validation failure + */ + static AttrInactivity createFromString(final String str) + throws BOSHException { + if (str == null) { + return null; + } else { + return new AttrInactivity(str); + } + } + +} diff --git a/src/com/kenai/jbosh/AttrMaxPause.java b/src/com/kenai/jbosh/AttrMaxPause.java new file mode 100644 index 0000000..8d1d98b --- /dev/null +++ b/src/com/kenai/jbosh/AttrMaxPause.java @@ -0,0 +1,65 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +import java.util.concurrent.TimeUnit; + +/** + * Data type representing the getValue of the {@code maxpause} attribute of the + * {@code bosh} element. + */ +final class AttrMaxPause extends AbstractIntegerAttr { + + /** + * Creates a new attribute object. + * + * @param val attribute getValue + * @throws BOSHException on parse or validation failure + */ + private AttrMaxPause(final String val) throws BOSHException { + super(val); + checkMinValue(1); + } + + /** + * Creates a new attribute instance from the provided String. + * + * @param str string representation of the attribute + * @return attribute instance or {@code null} if provided string is + * {@code null} + * @throws BOSHException on parse or validation failure + */ + static AttrMaxPause createFromString(final String str) + throws BOSHException { + if (str == null) { + return null; + } else { + return new AttrMaxPause(str); + } + } + + /** + * Get the max pause time in milliseconds. + * + * @return pause tme in milliseconds + */ + public int getInMilliseconds() { + return (int) TimeUnit.MILLISECONDS.convert( + intValue(), TimeUnit.SECONDS); + } + +} diff --git a/src/com/kenai/jbosh/AttrPause.java b/src/com/kenai/jbosh/AttrPause.java new file mode 100644 index 0000000..5fb3282 --- /dev/null +++ b/src/com/kenai/jbosh/AttrPause.java @@ -0,0 +1,65 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +import java.util.concurrent.TimeUnit; + +/** + * Data type representing the getValue of the {@code pause} attribute of the + * {@code bosh} element. + */ +final class AttrPause extends AbstractIntegerAttr { + + /** + * Creates a new attribute object. + * + * @param val attribute getValue + * @throws BOSHException on parse or validation failure + */ + private AttrPause(final String val) throws BOSHException { + super(val); + checkMinValue(1); + } + + /** + * Creates a new attribute instance from the provided String. + * + * @param str string representation of the attribute + * @return attribute instance or {@code null} if provided string is + * {@code null} + * @throws BOSHException on parse or validation failure + */ + static AttrPause createFromString(final String str) + throws BOSHException { + if (str == null) { + return null; + } else { + return new AttrPause(str); + } + } + + /** + * Get the pause time in milliseconds. + * + * @return pause tme in milliseconds + */ + public int getInMilliseconds() { + return (int) TimeUnit.MILLISECONDS.convert( + intValue(), TimeUnit.SECONDS); + } + +} diff --git a/src/com/kenai/jbosh/AttrPolling.java b/src/com/kenai/jbosh/AttrPolling.java new file mode 100644 index 0000000..3f0b08d --- /dev/null +++ b/src/com/kenai/jbosh/AttrPolling.java @@ -0,0 +1,65 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +import java.util.concurrent.TimeUnit; + +/** + * Data type representing the getValue of the {@code polling} attribute of the + * {@code bosh} element. + */ +final class AttrPolling extends AbstractIntegerAttr { + + /** + * Creates a new attribute object. + * + * @param val attribute getValue + * @throws BOSHException on parse or validation failure + */ + private AttrPolling(final String str) throws BOSHException { + super(str); + checkMinValue(0); + } + + /** + * Creates a new attribute instance from the provided String. + * + * @param str string representation of the attribute + * @return instance of the attribute for the specified string, or + * {@code null} if input string is {@code null} + * @throws BOSHException on parse or validation failure + */ + static AttrPolling createFromString(final String str) + throws BOSHException { + if (str == null) { + return null; + } else { + return new AttrPolling(str); + } + } + + /** + * Get the polling interval in milliseconds. + * + * @return polling interval in milliseconds + */ + public int getInMilliseconds() { + return (int) TimeUnit.MILLISECONDS.convert( + intValue(), TimeUnit.SECONDS); + } + +} diff --git a/src/com/kenai/jbosh/AttrRequests.java b/src/com/kenai/jbosh/AttrRequests.java new file mode 100644 index 0000000..bfdc529 --- /dev/null +++ b/src/com/kenai/jbosh/AttrRequests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +/** + * Data type representing the value of the {@code requests} attribute of the + * {@code bosh} element. + */ +final class AttrRequests extends AbstractIntegerAttr { + + /** + * Creates a new attribute object. + * + * @param val attribute value + * @throws BOSHException on parse or validation failure + */ + private AttrRequests(final String val) throws BOSHException { + super(val); + checkMinValue(1); + } + + /** + * Creates a new attribute instance from the provided String. + * + * @param str string representation of the attribute + * @return instance of the attribute for the specified string, or + * {@code null} if input string is {@code null} + * @throws BOSHException on parse or validation failure + */ + static AttrRequests createFromString(final String str) + throws BOSHException { + if (str == null) { + return null; + } else { + return new AttrRequests(str); + } + } + +} diff --git a/src/com/kenai/jbosh/AttrSessionID.java b/src/com/kenai/jbosh/AttrSessionID.java new file mode 100644 index 0000000..1998968 --- /dev/null +++ b/src/com/kenai/jbosh/AttrSessionID.java @@ -0,0 +1,44 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +/** + * Data type representing the getValue of the {@code sid} attribute of the + * {@code bosh} element. + */ +final class AttrSessionID extends AbstractAttr<String> { + + /** + * Creates a new attribute object. + * + * @param val attribute getValue + */ + private AttrSessionID(final String val) { + super(val); + } + + /** + * Creates a new attribute instance from the provided String. + * + * @param str string representation of the attribute + * @return attribute instance + */ + static AttrSessionID createFromString(final String str) { + return new AttrSessionID(str); + } + +} diff --git a/src/com/kenai/jbosh/AttrVersion.java b/src/com/kenai/jbosh/AttrVersion.java new file mode 100644 index 0000000..9396e3b --- /dev/null +++ b/src/com/kenai/jbosh/AttrVersion.java @@ -0,0 +1,165 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +/** + * Data type representing the getValue of the {@code ver} attribute of the + * {@code bosh} element. + */ +final class AttrVersion extends AbstractAttr<String> implements Comparable { + + /** + * Default value if none is provided. + */ + private static final AttrVersion DEFAULT; + static { + try { + DEFAULT = createFromString("1.8"); + } catch (BOSHException boshx) { + throw(new IllegalStateException(boshx)); + } + } + + /** + * Major portion of the version. + */ + private final int major; + + /** + * Minor portion of the version. + */ + private final int minor; + + /** + * Creates a new attribute object. + * + * @param val attribute getValue + * @throws BOSHException on parse or validation failure + */ + private AttrVersion(final String val) throws BOSHException { + super(val); + + int idx = val.indexOf('.'); + if (idx <= 0) { + throw(new BOSHException( + "Illegal ver attribute value (not in major.minor form): " + + val)); + } + + String majorStr = val.substring(0, idx); + try { + major = Integer.parseInt(majorStr); + } catch (NumberFormatException nfx) { + throw(new BOSHException( + "Could not parse ver attribute value (major ver): " + + majorStr, + nfx)); + } + if (major < 0) { + throw(new BOSHException( + "Major version may not be < 0")); + } + + String minorStr = val.substring(idx + 1); + try { + minor = Integer.parseInt(minorStr); + } catch (NumberFormatException nfx) { + throw(new BOSHException( + "Could not parse ver attribute value (minor ver): " + + minorStr, + nfx)); + } + if (minor < 0) { + throw(new BOSHException( + "Minor version may not be < 0")); + } + } + + /** + * Get the version of specifcation that we support. + * + * @return max spec version the code supports + */ + static AttrVersion getSupportedVersion() { + return DEFAULT; + } + + /** + * Creates a new attribute instance from the provided String. + * + * @param str string representation of the attribute + * @return attribute instance or {@code null} if provided string is + * {@code null} + * @throws BOSHException on parse or validation failure + */ + static AttrVersion createFromString(final String str) + throws BOSHException { + if (str == null) { + return null; + } else { + return new AttrVersion(str); + } + } + + /** + * Returns the 'major' portion of the version number. + * + * @return major digits only + */ + int getMajor() { + return major; + } + + /** + * Returns the 'minor' portion of the version number. + * + * @return minor digits only + */ + int getMinor() { + return minor; + } + + /////////////////////////////////////////////////////////////////////////// + // Comparable interface: + + /** + * {@inheritDoc} + * + * @param otherObj object to compare to + * @return -1, 0, or 1 + */ + @Override + public int compareTo(final Object otherObj) { + if (otherObj instanceof AttrVersion) { + AttrVersion other = (AttrVersion) otherObj; + if (major < other.major) { + return -1; + } else if (major > other.major) { + return 1; + } else if (minor < other.minor) { + return -1; + } else if (minor > other.minor) { + return 1; + } else { + return 0; + } + } else { + return 0; + } + } + +} diff --git a/src/com/kenai/jbosh/AttrWait.java b/src/com/kenai/jbosh/AttrWait.java new file mode 100644 index 0000000..d2c95f7 --- /dev/null +++ b/src/com/kenai/jbosh/AttrWait.java @@ -0,0 +1,53 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +/** + * Data type representing the getValue of the {@code wait} attribute of the + * {@code bosh} element. + */ +final class AttrWait extends AbstractIntegerAttr { + + /** + * Creates a new attribute object. + * + * @param val attribute getValue + * @throws BOSHException on parse or validation failure + */ + private AttrWait(final String val) throws BOSHException { + super(val); + checkMinValue(1); + } + + /** + * Creates a new attribute instance from the provided String. + * + * @param str string representation of the attribute + * @return attribute instance or {@code null} if provided string is + * {@code null} + * @throws BOSHException on parse or validation failure + */ + static AttrWait createFromString(final String str) + throws BOSHException { + if (str == null) { + return null; + } else { + return new AttrWait(str); + } + } + +} diff --git a/src/com/kenai/jbosh/Attributes.java b/src/com/kenai/jbosh/Attributes.java new file mode 100644 index 0000000..d01541e --- /dev/null +++ b/src/com/kenai/jbosh/Attributes.java @@ -0,0 +1,64 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +import javax.xml.XMLConstants; + +/** + * Class containing constants for attribute definitions used by the + * XEP-0124 specification. We shouldn't need to expose these outside + * our package, since nobody else should be needing to worry about + * them. + */ +final class Attributes { + + /** + * Private constructor to prevent construction of library class. + */ + private Attributes() { + super(); + } + + static final BodyQName ACCEPT = BodyQName.createBOSH("accept"); + static final BodyQName AUTHID = BodyQName.createBOSH("authid"); + static final BodyQName ACK = BodyQName.createBOSH("ack"); + static final BodyQName CHARSETS = BodyQName.createBOSH("charsets"); + static final BodyQName CONDITION = BodyQName.createBOSH("condition"); + static final BodyQName CONTENT = BodyQName.createBOSH("content"); + static final BodyQName FROM = BodyQName.createBOSH("from"); + static final BodyQName HOLD = BodyQName.createBOSH("hold"); + static final BodyQName INACTIVITY = BodyQName.createBOSH("inactivity"); + static final BodyQName KEY = BodyQName.createBOSH("key"); + static final BodyQName MAXPAUSE = BodyQName.createBOSH("maxpause"); + static final BodyQName NEWKEY = BodyQName.createBOSH("newkey"); + static final BodyQName PAUSE = BodyQName.createBOSH("pause"); + static final BodyQName POLLING = BodyQName.createBOSH("polling"); + static final BodyQName REPORT = BodyQName.createBOSH("report"); + static final BodyQName REQUESTS = BodyQName.createBOSH("requests"); + static final BodyQName RID = BodyQName.createBOSH("rid"); + static final BodyQName ROUTE = BodyQName.createBOSH("route"); + static final BodyQName SECURE = BodyQName.createBOSH("secure"); + static final BodyQName SID = BodyQName.createBOSH("sid"); + static final BodyQName STREAM = BodyQName.createBOSH("stream"); + static final BodyQName TIME = BodyQName.createBOSH("time"); + static final BodyQName TO = BodyQName.createBOSH("to"); + static final BodyQName TYPE = BodyQName.createBOSH("type"); + static final BodyQName VER = BodyQName.createBOSH("ver"); + static final BodyQName WAIT = BodyQName.createBOSH("wait"); + static final BodyQName XML_LANG = + BodyQName.createWithPrefix(XMLConstants.XML_NS_URI, "lang", "xml"); +} diff --git a/src/com/kenai/jbosh/BOSHClient.java b/src/com/kenai/jbosh/BOSHClient.java new file mode 100644 index 0000000..b96d188 --- /dev/null +++ b/src/com/kenai/jbosh/BOSHClient.java @@ -0,0 +1,1536 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +import com.kenai.jbosh.ComposableBody.Builder; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * BOSH Client session instance. Each communication session with a remote + * connection manager is represented and handled by an instance of this + * class. This is the main entry point for client-side communications. + * To create a new session, a client configuration must first be created + * and then used to create a client instance: + * <pre> + * BOSHClientConfig cfg = BOSHClientConfig.Builder.create( + * "http://server:1234/httpbind", "jabber.org") + * .setFrom("user@jabber.org") + * .build(); + * BOSHClient client = BOSHClient.create(cfg); + * </pre> + * Additional client configuration options are available. See the + * {@code BOSHClientConfig.Builder} class for more information. + * <p/> + * Once a {@code BOSHClient} instance has been created, communication with + * the remote connection manager can begin. No attempt will be made to + * establish a connection to the connection manager until the first call + * is made to the {@code send(ComposableBody)} method. Note that it is + * possible to send an empty body to cause an immediate connection attempt + * to the connection manager. Sending an empty message would look like + * the following: + * <pre> + * client.send(ComposableBody.builder().build()); + * </pre> + * For more information on creating body messages with content, see the + * {@code ComposableBody.Builder} class documentation. + * <p/> + * Once a session has been successfully started, the client instance can be + * used to send arbitrary payload data. All aspects of the BOSH + * protocol involving setting and processing attributes in the BOSH + * namespace will be handled by the client code transparently and behind the + * scenes. The user of the client instance can therefore concentrate + * entirely on the content of the message payload, leaving the semantics of + * the BOSH protocol to the client implementation. + * <p/> + * To be notified of incoming messages from the remote connection manager, + * a {@code BOSHClientResponseListener} should be added to the client instance. + * All incoming messages will be published to all response listeners as they + * arrive and are processed. As with the transmission of payload data via + * the {@code send(ComposableBody)} method, there is no need to worry about + * handling of the BOSH attributes, since this is handled behind the scenes. + * <p/> + * If the connection to the remote connection manager is terminated (either + * explicitly or due to a terminal condition of some sort), all connection + * listeners will be notified. After the connection has been closed, the + * client instance is considered dead and a new one must be created in order + * to resume communications with the remote server. + * <p/> + * Instances of this class are thread-safe. + * + * @see BOSHClientConfig.Builder + * @see BOSHClientResponseListener + * @see BOSHClientConnListener + * @see ComposableBody.Builder + */ +public final class BOSHClient { + + /** + * Logger. + */ + private static final Logger LOG = Logger.getLogger( + BOSHClient.class.getName()); + + /** + * Value of the 'type' attribute used for session termination. + */ + private static final String TERMINATE = "terminate"; + + /** + * Value of the 'type' attribute used for recoverable errors. + */ + private static final String ERROR = "error"; + + /** + * Message to use for interrupted exceptions. + */ + private static final String INTERRUPTED = "Interrupted"; + + /** + * Message used for unhandled exceptions. + */ + private static final String UNHANDLED = "Unhandled Exception"; + + /** + * Message used whena null listener is detected. + */ + private static final String NULL_LISTENER = "Listener may not b enull"; + + /** + * Default empty request delay. + */ + private static final int DEFAULT_EMPTY_REQUEST_DELAY = 100; + + /** + * Amount of time to wait before sending an empty request, in + * milliseconds. + */ + private static final int EMPTY_REQUEST_DELAY = Integer.getInteger( + BOSHClient.class.getName() + ".emptyRequestDelay", + DEFAULT_EMPTY_REQUEST_DELAY); + + /** + * Default value for the pause margin. + */ + private static final int DEFAULT_PAUSE_MARGIN = 500; + + /** + * The amount of time in milliseconds which will be reserved as a + * safety margin when scheduling empty requests against a maxpause + * value. This should give us enough time to build the message + * and transport it to the remote host. + */ + private static final int PAUSE_MARGIN = Integer.getInteger( + BOSHClient.class.getName() + ".pauseMargin", + DEFAULT_PAUSE_MARGIN); + + /** + * Flag indicating whether or not we want to perform assertions. + */ + private static final boolean ASSERTIONS; + + /** + * Connection listeners. + */ + private final Set<BOSHClientConnListener> connListeners = + new CopyOnWriteArraySet<BOSHClientConnListener>(); + + /** + * Request listeners. + */ + private final Set<BOSHClientRequestListener> requestListeners = + new CopyOnWriteArraySet<BOSHClientRequestListener>(); + + /** + * Response listeners. + */ + private final Set<BOSHClientResponseListener> responseListeners = + new CopyOnWriteArraySet<BOSHClientResponseListener>(); + + /** + * Lock instance. + */ + private final ReentrantLock lock = new ReentrantLock(); + + /** + * Condition indicating that there are messages to be exchanged. + */ + private final Condition notEmpty = lock.newCondition(); + + /** + * Condition indicating that there are available slots for sending + * messages. + */ + private final Condition notFull = lock.newCondition(); + + /** + * Condition indicating that there are no outstanding connections. + */ + private final Condition drained = lock.newCondition(); + + /** + * Session configuration. + */ + private final BOSHClientConfig cfg; + + /** + * Processor thread runnable instance. + */ + private final Runnable procRunnable = new Runnable() { + /** + * Process incoming messages. + */ + public void run() { + processMessages(); + } + }; + + /** + * Processor thread runnable instance. + */ + private final Runnable emptyRequestRunnable = new Runnable() { + /** + * Process incoming messages. + */ + public void run() { + sendEmptyRequest(); + } + }; + + /** + * HTTPSender instance. + */ + private final HTTPSender httpSender = + new ApacheHTTPSender(); + + /** + * Storage for test hook implementation. + */ + private final AtomicReference<ExchangeInterceptor> exchInterceptor = + new AtomicReference<ExchangeInterceptor>(); + + /** + * Request ID sequence to use for the session. + */ + private final RequestIDSequence requestIDSeq = new RequestIDSequence(); + + /** + * ScheduledExcecutor to use for deferred tasks. + */ + private final ScheduledExecutorService schedExec = + Executors.newSingleThreadScheduledExecutor(); + + /************************************************************ + * The following vars must be accessed via the lock instance. + */ + + /** + * Thread which is used to process responses from the connection + * manager. Becomes null when session is terminated. + */ + private Thread procThread; + + /** + * Future for sending a deferred empty request, if needed. + */ + private ScheduledFuture emptyRequestFuture; + + /** + * Connection Manager session parameters. Only available when in a + * connected state. + */ + private CMSessionParams cmParams; + + /** + * List of active/outstanding requests. + */ + private Queue<HTTPExchange> exchanges = new LinkedList<HTTPExchange>(); + + /** + * Set of RIDs which have been received, for the purpose of sending + * response acknowledgements. + */ + private SortedSet<Long> pendingResponseAcks = new TreeSet<Long>(); + + /** + * The highest RID that we've already received a response for. This value + * is used to implement response acks. + */ + private Long responseAck = Long.valueOf(-1L); + + /** + * List of requests which have been made but not yet acknowledged. This + * list remains unpopulated if the CM is not acking requests. + */ + private List<ComposableBody> pendingRequestAcks = + new ArrayList<ComposableBody>(); + + /////////////////////////////////////////////////////////////////////////// + // Classes: + + /** + * Class used in testing to dynamically manipulate received exchanges + * at test runtime. + */ + abstract static class ExchangeInterceptor { + /** + * Limit construction. + */ + ExchangeInterceptor() { + // Empty; + } + + /** + * Hook to manipulate an HTTPExchange as is is about to be processed. + * + * @param exch original exchange that would be processed + * @return replacement exchange instance, or {@code null} to skip + * processing of this exchange + */ + abstract HTTPExchange interceptExchange(final HTTPExchange exch); + } + + /////////////////////////////////////////////////////////////////////////// + // Constructors: + + /** + * Determine whether or not we should perform assertions. Assertions + * can be specified via system property explicitly, or defaulted to + * the JVM assertions status. + */ + static { + final String prop = + BOSHClient.class.getSimpleName() + ".assertionsEnabled"; + boolean enabled = false; + if (System.getProperty(prop) == null) { + assert enabled = true; + } else { + enabled = Boolean.getBoolean(prop); + } + ASSERTIONS = enabled; + } + + /** + * Prevent direct construction. + */ + private BOSHClient(final BOSHClientConfig sessCfg) { + cfg = sessCfg; + init(); + } + + /////////////////////////////////////////////////////////////////////////// + // Public methods: + + /** + * Create a new BOSH client session using the client configuration + * information provided. + * + * @param clientCfg session configuration + * @return BOSH session instance + */ + public static BOSHClient create(final BOSHClientConfig clientCfg) { + if (clientCfg == null) { + throw(new IllegalArgumentException( + "Client configuration may not be null")); + } + return new BOSHClient(clientCfg); + } + + /** + * Get the client configuration that was used to create this client + * instance. + * + * @return client configuration + */ + public BOSHClientConfig getBOSHClientConfig() { + return cfg; + } + + /** + * Adds a connection listener to the session. + * + * @param listener connection listener to add, if not already added + */ + public void addBOSHClientConnListener( + final BOSHClientConnListener listener) { + if (listener == null) { + throw(new IllegalArgumentException(NULL_LISTENER)); + } + connListeners.add(listener); + } + + /** + * Removes a connection listener from the session. + * + * @param listener connection listener to remove, if previously added + */ + public void removeBOSHClientConnListener( + final BOSHClientConnListener listener) { + if (listener == null) { + throw(new IllegalArgumentException(NULL_LISTENER)); + } + connListeners.remove(listener); + } + + /** + * Adds a request message listener to the session. + * + * @param listener request listener to add, if not already added + */ + public void addBOSHClientRequestListener( + final BOSHClientRequestListener listener) { + if (listener == null) { + throw(new IllegalArgumentException(NULL_LISTENER)); + } + requestListeners.add(listener); + } + + /** + * Removes a request message listener from the session, if previously + * added. + * + * @param listener instance to remove + */ + public void removeBOSHClientRequestListener( + final BOSHClientRequestListener listener) { + if (listener == null) { + throw(new IllegalArgumentException(NULL_LISTENER)); + } + requestListeners.remove(listener); + } + + /** + * Adds a response message listener to the session. + * + * @param listener response listener to add, if not already added + */ + public void addBOSHClientResponseListener( + final BOSHClientResponseListener listener) { + if (listener == null) { + throw(new IllegalArgumentException(NULL_LISTENER)); + } + responseListeners.add(listener); + } + + /** + * Removes a response message listener from the session, if previously + * added. + * + * @param listener instance to remove + */ + public void removeBOSHClientResponseListener( + final BOSHClientResponseListener listener) { + if (listener == null) { + throw(new IllegalArgumentException(NULL_LISTENER)); + } + responseListeners.remove(listener); + } + + /** + * Send the provided message data to the remote connection manager. The + * provided message body does not need to have any BOSH-specific attribute + * information set. It only needs to contain the actual message payload + * that should be delivered to the remote server. + * <p/> + * The first call to this method will result in a connection attempt + * to the remote connection manager. Subsequent calls to this method + * will block until the underlying session state allows for the message + * to be transmitted. In certain scenarios - such as when the maximum + * number of outbound connections has been reached - calls to this method + * will block for short periods of time. + * + * @param body message data to send to remote server + * @throws BOSHException on message transmission failure + */ + public void send(final ComposableBody body) throws BOSHException { + assertUnlocked(); + if (body == null) { + throw(new IllegalArgumentException( + "Message body may not be null")); + } + + HTTPExchange exch; + CMSessionParams params; + lock.lock(); + try { + blockUntilSendable(body); + if (!isWorking() && !isTermination(body)) { + throw(new BOSHException( + "Cannot send message when session is closed")); + } + + long rid = requestIDSeq.getNextRID(); + ComposableBody request = body; + params = cmParams; + if (params == null && exchanges.isEmpty()) { + // This is the first message being sent + request = applySessionCreationRequest(rid, body); + } else { + request = applySessionData(rid, body); + if (cmParams.isAckingRequests()) { + pendingRequestAcks.add(request); + } + } + exch = new HTTPExchange(request); + exchanges.add(exch); + notEmpty.signalAll(); + clearEmptyRequest(); + } finally { + lock.unlock(); + } + AbstractBody finalReq = exch.getRequest(); + HTTPResponse resp = httpSender.send(params, finalReq); + exch.setHTTPResponse(resp); + fireRequestSent(finalReq); + } + + /** + * Attempt to pause the current session. When supported by the remote + * connection manager, pausing the session will result in the connection + * manager closing out all outstanding requests (including the pause + * request) and increases the inactivity timeout of the session. The + * exact value of the temporary timeout is dependent upon the connection + * manager. This method should be used if a client encounters an + * exceptional temporary situation during which it will be unable to send + * requests to the connection manager for a period of time greater than + * the maximum inactivity period. + * + * The session will revert back to it's normal, unpaused state when the + * client sends it's next message. + * + * @return {@code true} if the connection manager supports session pausing, + * {@code false} if the connection manager does not support session + * pausing or if the session has not yet been established + */ + public boolean pause() { + assertUnlocked(); + lock.lock(); + AttrMaxPause maxPause = null; + try { + if (cmParams == null) { + return false; + } + + maxPause = cmParams.getMaxPause(); + if (maxPause == null) { + return false; + } + } finally { + lock.unlock(); + } + try { + send(ComposableBody.builder() + .setAttribute(Attributes.PAUSE, maxPause.toString()) + .build()); + } catch (BOSHException boshx) { + LOG.log(Level.FINEST, "Could not send pause", boshx); + } + return true; + } + + /** + * End the BOSH session by disconnecting from the remote BOSH connection + * manager. + * + * @throws BOSHException when termination message cannot be sent + */ + public void disconnect() throws BOSHException { + disconnect(ComposableBody.builder().build()); + } + + /** + * End the BOSH session by disconnecting from the remote BOSH connection + * manager, sending the provided content in the final connection + * termination message. + * + * @param msg final message to send + * @throws BOSHException when termination message cannot be sent + */ + public void disconnect(final ComposableBody msg) throws BOSHException { + if (msg == null) { + throw(new IllegalArgumentException( + "Message body may not be null")); + } + + Builder builder = msg.rebuild(); + builder.setAttribute(Attributes.TYPE, TERMINATE); + send(builder.build()); + } + + /** + * Forcibly close this client session instance. The preferred mechanism + * to close the connection is to send a disconnect message and wait for + * organic termination. Calling this method simply shuts down the local + * session without sending a termination message, releasing all resources + * associated with the session. + */ + public void close() { + dispose(new BOSHException("Session explicitly closed by caller")); + } + + /////////////////////////////////////////////////////////////////////////// + // Package-private methods: + + /** + * Get the current CM session params. + * + * @return current session params, or {@code null} + */ + CMSessionParams getCMSessionParams() { + lock.lock(); + try { + return cmParams; + } finally { + lock.unlock(); + } + } + + /** + * Wait until no more messages are waiting to be processed. + */ + void drain() { + lock.lock(); + try { + LOG.finest("Waiting while draining..."); + while (isWorking() + && (emptyRequestFuture == null + || emptyRequestFuture.isDone())) { + try { + drained.await(); + } catch (InterruptedException intx) { + LOG.log(Level.FINEST, INTERRUPTED, intx); + } + } + LOG.finest("Drained"); + } finally { + lock.unlock(); + } + } + + /** + * Test method used to forcibly discard next exchange. + * + * @param interceptor exchange interceptor + */ + void setExchangeInterceptor(final ExchangeInterceptor interceptor) { + exchInterceptor.set(interceptor); + } + + + /////////////////////////////////////////////////////////////////////////// + // Private methods: + + /** + * Initialize the session. This initializes the underlying HTTP + * transport implementation and starts the receive thread. + */ + private void init() { + assertUnlocked(); + + lock.lock(); + try { + httpSender.init(cfg); + procThread = new Thread(procRunnable); + procThread.setDaemon(true); + procThread.setName(BOSHClient.class.getSimpleName() + + "[" + System.identityHashCode(this) + + "]: Receive thread"); + procThread.start(); + } finally { + lock.unlock(); + } + } + + /** + * Destroy this session. + * + * @param cause the reason for the session termination, or {@code null} + * for normal termination + */ + private void dispose(final Throwable cause) { + assertUnlocked(); + + lock.lock(); + try { + if (procThread == null) { + // Already disposed + return; + } + procThread = null; + } finally { + lock.unlock(); + } + + if (cause == null) { + fireConnectionClosed(); + } else { + fireConnectionClosedOnError(cause); + } + + lock.lock(); + try { + clearEmptyRequest(); + exchanges = null; + cmParams = null; + pendingResponseAcks = null; + pendingRequestAcks = null; + notEmpty.signalAll(); + notFull.signalAll(); + drained.signalAll(); + } finally { + lock.unlock(); + } + + httpSender.destroy(); + schedExec.shutdownNow(); + } + + /** + * Determines if the message body specified indicates a request to + * pause the session. + * + * @param msg message to evaluate + * @return {@code true} if the message is a pause request, {@code false} + * otherwise + */ + private static boolean isPause(final AbstractBody msg) { + return msg.getAttribute(Attributes.PAUSE) != null; + } + + /** + * Determines if the message body specified indicates a termination of + * the session. + * + * @param msg message to evaluate + * @return {@code true} if the message is a session termination, + * {@code false} otherwise + */ + private static boolean isTermination(final AbstractBody msg) { + return TERMINATE.equals(msg.getAttribute(Attributes.TYPE)); + } + + /** + * Evaluates the HTTP response code and response message and returns the + * terminal binding condition that it describes, if any. + * + * @param respCode HTTP response code + * @param respBody response body + * @return terminal binding condition, or {@code null} if not a terminal + * binding condition message + */ + private TerminalBindingCondition getTerminalBindingCondition( + final int respCode, + final AbstractBody respBody) { + assertLocked(); + + if (isTermination(respBody)) { + String str = respBody.getAttribute(Attributes.CONDITION); + return TerminalBindingCondition.forString(str); + } + // Check for deprecated HTTP Error Conditions + if (cmParams != null && cmParams.getVersion() == null) { + return TerminalBindingCondition.forHTTPResponseCode(respCode); + } + return null; + } + + /** + * Determines if the message specified is immediately sendable or if it + * needs to block until the session state changes. + * + * @param msg message to evaluate + * @return {@code true} if the message can be immediately sent, + * {@code false} otherwise + */ + private boolean isImmediatelySendable(final AbstractBody msg) { + assertLocked(); + + if (cmParams == null) { + // block if we're waiting for a response to our first request + return exchanges.isEmpty(); + } + + AttrRequests requests = cmParams.getRequests(); + if (requests == null) { + return true; + } + int maxRequests = requests.intValue(); + if (exchanges.size() < maxRequests) { + return true; + } + if (exchanges.size() == maxRequests + && (isTermination(msg) || isPause(msg))) { + // One additional terminate or pause message is allowed + return true; + } + return false; + } + + /** + * Determines whether or not the session is still active. + * + * @return {@code true} if it is, {@code false} otherwise + */ + private boolean isWorking() { + assertLocked(); + + return procThread != null; + } + + /** + * Blocks until either the message provided becomes immediately + * sendable or until the session is terminated. + * + * @param msg message to evaluate + */ + private void blockUntilSendable(final AbstractBody msg) { + assertLocked(); + + while (isWorking() && !isImmediatelySendable(msg)) { + try { + notFull.await(); + } catch (InterruptedException intx) { + LOG.log(Level.FINEST, INTERRUPTED, intx); + } + } + } + + /** + * Modifies the specified body message such that it becomes a new + * BOSH session creation request. + * + * @param rid request ID to use + * @param orig original body to modify + * @return modified message which acts as a session creation request + */ + private ComposableBody applySessionCreationRequest( + final long rid, final ComposableBody orig) throws BOSHException { + assertLocked(); + + Builder builder = orig.rebuild(); + builder.setAttribute(Attributes.TO, cfg.getTo()); + builder.setAttribute(Attributes.XML_LANG, cfg.getLang()); + builder.setAttribute(Attributes.VER, + AttrVersion.getSupportedVersion().toString()); + builder.setAttribute(Attributes.WAIT, "60"); + builder.setAttribute(Attributes.HOLD, "1"); + builder.setAttribute(Attributes.RID, Long.toString(rid)); + applyRoute(builder); + applyFrom(builder); + builder.setAttribute(Attributes.ACK, "1"); + + // Make sure the following are NOT present (i.e., during retries) + builder.setAttribute(Attributes.SID, null); + return builder.build(); + } + + /** + * Applies routing information to the request message who's builder has + * been provided. + * + * @param builder builder instance to add routing information to + */ + private void applyRoute(final Builder builder) { + assertLocked(); + + String route = cfg.getRoute(); + if (route != null) { + builder.setAttribute(Attributes.ROUTE, route); + } + } + + /** + * Applies the local station ID information to the request message who's + * builder has been provided. + * + * @param builder builder instance to add station ID information to + */ + private void applyFrom(final Builder builder) { + assertLocked(); + + String from = cfg.getFrom(); + if (from != null) { + builder.setAttribute(Attributes.FROM, from); + } + } + + /** + * Applies existing session data to the outbound request, returning the + * modified request. + * + * This method assumes the lock is currently held. + * + * @param rid request ID to use + * @param orig original/raw request + * @return modified request with session information applied + */ + private ComposableBody applySessionData( + final long rid, + final ComposableBody orig) throws BOSHException { + assertLocked(); + + Builder builder = orig.rebuild(); + builder.setAttribute(Attributes.SID, + cmParams.getSessionID().toString()); + builder.setAttribute(Attributes.RID, Long.toString(rid)); + applyResponseAcknowledgement(builder, rid); + return builder.build(); + } + + /** + * Sets the 'ack' attribute of the request to the value of the highest + * 'rid' of a request for which it has already received a response in the + * case where it has also received all responses associated with lower + * 'rid' values. The only exception is that, after its session creation + * request, the client SHOULD NOT include an 'ack' attribute in any request + * if it has received responses to all its previous requests. + * + * @param builder message builder + * @param rid current request RID + */ + private void applyResponseAcknowledgement( + final Builder builder, + final long rid) { + assertLocked(); + + if (responseAck.equals(Long.valueOf(-1L))) { + // We have not received any responses yet + return; + } + + Long prevRID = Long.valueOf(rid - 1L); + if (responseAck.equals(prevRID)) { + // Implicit ack + return; + } + + builder.setAttribute(Attributes.ACK, responseAck.toString()); + } + + /** + * While we are "connected", process received responses. + * + * This method is run in the processing thread. + */ + private void processMessages() { + LOG.log(Level.FINEST, "Processing thread starting"); + try { + HTTPExchange exch; + do { + exch = nextExchange(); + if (exch == null) { + break; + } + + // Test hook to manipulate what the client sees: + ExchangeInterceptor interceptor = exchInterceptor.get(); + if (interceptor != null) { + HTTPExchange newExch = interceptor.interceptExchange(exch); + if (newExch == null) { + LOG.log(Level.FINE, "Discarding exchange on request " + + "of test hook: RID=" + + exch.getRequest().getAttribute( + Attributes.RID)); + lock.lock(); + try { + exchanges.remove(exch); + } finally { + lock.unlock(); + } + continue; + } + exch = newExch; + } + + processExchange(exch); + } while (true); + } finally { + LOG.log(Level.FINEST, "Processing thread exiting"); + } + + } + + /** + * Get the next message exchange to process, blocking until one becomes + * available if nothing is already waiting for processing. + * + * @return next available exchange to process, or {@code null} if no + * exchanges are immediately available + */ + private HTTPExchange nextExchange() { + assertUnlocked(); + + final Thread thread = Thread.currentThread(); + HTTPExchange exch = null; + lock.lock(); + try { + do { + if (!thread.equals(procThread)) { + break; + } + exch = exchanges.peek(); + if (exch == null) { + try { + notEmpty.await(); + } catch (InterruptedException intx) { + LOG.log(Level.FINEST, INTERRUPTED, intx); + } + } + } while (exch == null); + } finally { + lock.unlock(); + } + return exch; + } + + /** + * Process the next, provided exchange. This is the main processing + * method of the receive thread. + * + * @param exch message exchange to process + */ + private void processExchange(final HTTPExchange exch) { + assertUnlocked(); + + HTTPResponse resp; + AbstractBody body; + int respCode; + try { + resp = exch.getHTTPResponse(); + body = resp.getBody(); + respCode = resp.getHTTPStatus(); + } catch (BOSHException boshx) { + LOG.log(Level.FINEST, "Could not obtain response", boshx); + dispose(boshx); + return; + } catch (InterruptedException intx) { + LOG.log(Level.FINEST, INTERRUPTED, intx); + dispose(intx); + return; + } + fireResponseReceived(body); + + // Process the message with the current session state + AbstractBody req = exch.getRequest(); + CMSessionParams params; + List<HTTPExchange> toResend = null; + lock.lock(); + try { + // Check for session creation response info, if needed + if (cmParams == null) { + cmParams = CMSessionParams.fromSessionInit(req, body); + + // The following call handles the lock. It's not an escape. + fireConnectionEstablished(); + } + params = cmParams; + + checkForTerminalBindingConditions(body, respCode); + if (isTermination(body)) { + // Explicit termination + lock.unlock(); + dispose(null); + return; + } + + if (isRecoverableBindingCondition(body)) { + // Retransmit outstanding requests + if (toResend == null) { + toResend = new ArrayList<HTTPExchange>(exchanges.size()); + } + for (HTTPExchange exchange : exchanges) { + HTTPExchange resendExch = + new HTTPExchange(exchange.getRequest()); + toResend.add(resendExch); + } + for (HTTPExchange exchange : toResend) { + exchanges.add(exchange); + } + } else { + // Process message as normal + processRequestAcknowledgements(req, body); + processResponseAcknowledgementData(req); + HTTPExchange resendExch = + processResponseAcknowledgementReport(body); + if (resendExch != null && toResend == null) { + toResend = new ArrayList<HTTPExchange>(1); + toResend.add(resendExch); + exchanges.add(resendExch); + } + } + } catch (BOSHException boshx) { + LOG.log(Level.FINEST, "Could not process response", boshx); + lock.unlock(); + dispose(boshx); + return; + } finally { + if (lock.isHeldByCurrentThread()) { + try { + exchanges.remove(exch); + if (exchanges.isEmpty()) { + scheduleEmptyRequest(processPauseRequest(req)); + } + notFull.signalAll(); + } finally { + lock.unlock(); + } + } + } + + if (toResend != null) { + for (HTTPExchange resend : toResend) { + HTTPResponse response = + httpSender.send(params, resend.getRequest()); + resend.setHTTPResponse(response); + fireRequestSent(resend.getRequest()); + } + } + } + + /** + * Clears any scheduled empty requests. + */ + private void clearEmptyRequest() { + assertLocked(); + + if (emptyRequestFuture != null) { + emptyRequestFuture.cancel(false); + emptyRequestFuture = null; + } + } + + /** + * Calculates the default empty request delay/interval to use for the + * active session. + * + * @return delay in milliseconds + */ + private long getDefaultEmptyRequestDelay() { + assertLocked(); + + // Figure out how long we should wait before sending an empty request + AttrPolling polling = cmParams.getPollingInterval(); + long delay; + if (polling == null) { + delay = EMPTY_REQUEST_DELAY; + } else { + delay = polling.getInMilliseconds(); + } + return delay; + } + + /** + * Schedule an empty request to be sent if no other requests are + * sent in a reasonable amount of time. + */ + private void scheduleEmptyRequest(long delay) { + assertLocked(); + if (delay < 0L) { + throw(new IllegalArgumentException( + "Empty request delay must be >= 0 (was: " + delay + ")")); + } + + clearEmptyRequest(); + if (!isWorking()) { + return; + } + + // Schedule the transmission + if (LOG.isLoggable(Level.FINER)) { + LOG.finer("Scheduling empty request in " + delay + "ms"); + } + try { + emptyRequestFuture = schedExec.schedule(emptyRequestRunnable, + delay, TimeUnit.MILLISECONDS); + } catch (RejectedExecutionException rex) { + LOG.log(Level.FINEST, "Could not schedule empty request", rex); + } + drained.signalAll(); + } + + /** + * Sends an empty request to maintain session requirements. If a request + * is sent within a reasonable time window, the empty request transmission + * will be cancelled. + */ + private void sendEmptyRequest() { + assertUnlocked(); + // Send an empty request + LOG.finest("Sending empty request"); + try { + send(ComposableBody.builder().build()); + } catch (BOSHException boshx) { + dispose(boshx); + } + } + + /** + * Assert that the internal lock is held. + */ + private void assertLocked() { + if (ASSERTIONS) { + if (!lock.isHeldByCurrentThread()) { + throw(new AssertionError("Lock is not held by current thread")); + } + return; + } + } + + /** + * Assert that the internal lock is *not* held. + */ + private void assertUnlocked() { + if (ASSERTIONS) { + if (lock.isHeldByCurrentThread()) { + throw(new AssertionError("Lock is held by current thread")); + } + return; + } + } + + /** + * Checks to see if the response indicates a terminal binding condition + * (as per XEP-0124 section 17). If it does, an exception is thrown. + * + * @param body response body to evaluate + * @param code HTTP response code + * @throws BOSHException if a terminal binding condition is detected + */ + private void checkForTerminalBindingConditions( + final AbstractBody body, + final int code) + throws BOSHException { + TerminalBindingCondition cond = + getTerminalBindingCondition(code, body); + if (cond != null) { + throw(new BOSHException( + "Terminal binding condition encountered: " + + cond.getCondition() + " (" + + cond.getMessage() + ")")); + } + } + + /** + * Determines whether or not the response indicates a recoverable + * binding condition (as per XEP-0124 section 17). + * + * @param resp response body + * @return {@code true} if it does, {@code false} otherwise + */ + private static boolean isRecoverableBindingCondition( + final AbstractBody resp) { + return ERROR.equals(resp.getAttribute(Attributes.TYPE)); + } + + /** + * Process the request to determine if the empty request delay + * can be determined by looking to see if the request is a pause + * request. If it can, the request's delay is returned, otherwise + * the default delay is returned. + * + * @return delay in milliseconds that should elapse prior to an + * empty message being sent + */ + private long processPauseRequest( + final AbstractBody req) { + assertLocked(); + + if (cmParams != null && cmParams.getMaxPause() != null) { + try { + AttrPause pause = AttrPause.createFromString( + req.getAttribute(Attributes.PAUSE)); + if (pause != null) { + long delay = pause.getInMilliseconds() - PAUSE_MARGIN; + if (delay < 0) { + delay = EMPTY_REQUEST_DELAY; + } + return delay; + } + } catch (BOSHException boshx) { + LOG.log(Level.FINEST, "Could not extract", boshx); + } + } + + return getDefaultEmptyRequestDelay(); + } + + /** + * Check the response for request acknowledgements and take appropriate + * action. + * + * This method assumes the lock is currently held. + * + * @param req request + * @param resp response + */ + private void processRequestAcknowledgements( + final AbstractBody req, final AbstractBody resp) { + assertLocked(); + + if (!cmParams.isAckingRequests()) { + return; + } + + // If a report or time attribute is set, we aren't acking anything + if (resp.getAttribute(Attributes.REPORT) != null) { + return; + } + + // Figure out what the highest acked RID is + String acked = resp.getAttribute(Attributes.ACK); + Long ackUpTo; + if (acked == null) { + // Implicit ack of all prior requests up until RID + ackUpTo = Long.parseLong(req.getAttribute(Attributes.RID)); + } else { + ackUpTo = Long.parseLong(acked); + } + + // Remove the acked requests from the list + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest("Removing pending acks up to: " + ackUpTo); + } + Iterator<ComposableBody> iter = pendingRequestAcks.iterator(); + while (iter.hasNext()) { + AbstractBody pending = iter.next(); + Long pendingRID = Long.parseLong( + pending.getAttribute(Attributes.RID)); + if (pendingRID.compareTo(ackUpTo) <= 0) { + iter.remove(); + } + } + } + + /** + * Process the response in order to update the response acknowlegement + * data. + * + * This method assumes the lock is currently held. + * + * @param req request + */ + private void processResponseAcknowledgementData( + final AbstractBody req) { + assertLocked(); + + Long rid = Long.parseLong(req.getAttribute(Attributes.RID)); + if (responseAck.equals(Long.valueOf(-1L))) { + // This is the first request + responseAck = rid; + } else { + pendingResponseAcks.add(rid); + // Remove up until the first missing response (or end of queue) + Long whileVal = responseAck; + while (whileVal.equals(pendingResponseAcks.first())) { + responseAck = whileVal; + pendingResponseAcks.remove(whileVal); + whileVal = Long.valueOf(whileVal.longValue() + 1); + } + } + } + + /** + * Process the response in order to check for and respond to any potential + * ack reports. + * + * This method assumes the lock is currently held. + * + * @param resp response + * @return exchange to transmit if a resend is to be performed, or + * {@code null} if no resend is necessary + * @throws BOSHException when a a retry is needed but cannot be performed + */ + private HTTPExchange processResponseAcknowledgementReport( + final AbstractBody resp) + throws BOSHException { + assertLocked(); + + String reportStr = resp.getAttribute(Attributes.REPORT); + if (reportStr == null) { + // No report on this message + return null; + } + + Long report = Long.parseLong(reportStr); + Long time = Long.parseLong(resp.getAttribute(Attributes.TIME)); + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("Received report of missing request (RID=" + + report + ", time=" + time + "ms)"); + } + + // Find the missing request + Iterator<ComposableBody> iter = pendingRequestAcks.iterator(); + AbstractBody req = null; + while (iter.hasNext() && req == null) { + AbstractBody pending = iter.next(); + Long pendingRID = Long.parseLong( + pending.getAttribute(Attributes.RID)); + if (report.equals(pendingRID)) { + req = pending; + } + } + + if (req == null) { + throw(new BOSHException("Report of missing message with RID '" + + reportStr + + "' but local copy of that request was not found")); + } + + // Resend the missing request + HTTPExchange exch = new HTTPExchange(req); + exchanges.add(exch); + notEmpty.signalAll(); + return exch; + } + + /** + * Notifies all request listeners that the specified request is being + * sent. + * + * @param request request being sent + */ + private void fireRequestSent(final AbstractBody request) { + assertUnlocked(); + + BOSHMessageEvent event = null; + for (BOSHClientRequestListener listener : requestListeners) { + if (event == null) { + event = BOSHMessageEvent.createRequestSentEvent(this, request); + } + try { + listener.requestSent(event); + } catch (Exception ex) { + LOG.log(Level.WARNING, UNHANDLED, ex); + } + } + } + + /** + * Notifies all response listeners that the specified response has been + * received. + * + * @param response response received + */ + private void fireResponseReceived(final AbstractBody response) { + assertUnlocked(); + + BOSHMessageEvent event = null; + for (BOSHClientResponseListener listener : responseListeners) { + if (event == null) { + event = BOSHMessageEvent.createResponseReceivedEvent( + this, response); + } + try { + listener.responseReceived(event); + } catch (Exception ex) { + LOG.log(Level.WARNING, UNHANDLED, ex); + } + } + } + + /** + * Notifies all connection listeners that the session has been successfully + * established. + */ + private void fireConnectionEstablished() { + final boolean hadLock = lock.isHeldByCurrentThread(); + if (hadLock) { + lock.unlock(); + } + try { + BOSHClientConnEvent event = null; + for (BOSHClientConnListener listener : connListeners) { + if (event == null) { + event = BOSHClientConnEvent + .createConnectionEstablishedEvent(this); + } + try { + listener.connectionEvent(event); + } catch (Exception ex) { + LOG.log(Level.WARNING, UNHANDLED, ex); + } + } + } finally { + if (hadLock) { + lock.lock(); + } + } + } + + /** + * Notifies all connection listeners that the session has been + * terminated normally. + */ + private void fireConnectionClosed() { + assertUnlocked(); + + BOSHClientConnEvent event = null; + for (BOSHClientConnListener listener : connListeners) { + if (event == null) { + event = BOSHClientConnEvent.createConnectionClosedEvent(this); + } + try { + listener.connectionEvent(event); + } catch (Exception ex) { + LOG.log(Level.WARNING, UNHANDLED, ex); + } + } + } + + /** + * Notifies all connection listeners that the session has been + * terminated due to the exceptional condition provided. + * + * @param cause cause of the termination + */ + private void fireConnectionClosedOnError( + final Throwable cause) { + assertUnlocked(); + + BOSHClientConnEvent event = null; + for (BOSHClientConnListener listener : connListeners) { + if (event == null) { + event = BOSHClientConnEvent + .createConnectionClosedOnErrorEvent( + this, pendingRequestAcks, cause); + } + try { + listener.connectionEvent(event); + } catch (Exception ex) { + LOG.log(Level.WARNING, UNHANDLED, ex); + } + } + } + +} diff --git a/src/com/kenai/jbosh/BOSHClientConfig.java b/src/com/kenai/jbosh/BOSHClientConfig.java new file mode 100644 index 0000000..23915b6 --- /dev/null +++ b/src/com/kenai/jbosh/BOSHClientConfig.java @@ -0,0 +1,446 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +import java.net.URI; +import javax.net.ssl.SSLContext; + +/** + * BOSH client configuration information. Instances of this class contain + * all information necessary to establish connectivity with a remote + * connection manager. + * <p/> + * Instances of this class are immutable, thread-safe, + * and can be re-used to configure multiple client session instances. + */ +public final class BOSHClientConfig { + + /** + * Connection manager URI. + */ + private final URI uri; + + /** + * Target domain. + */ + private final String to; + + /** + * Client ID of this station. + */ + private final String from; + + /** + * Default XML language. + */ + private final String lang; + + /** + * Routing information for messages sent to CM. + */ + private final String route; + + /** + * Proxy host. + */ + private final String proxyHost; + + /** + * Proxy port. + */ + private final int proxyPort; + + /** + * SSL context. + */ + private final SSLContext sslContext; + + /** + * Flag indicating that compression should be attempted, if possible. + */ + private final boolean compressionEnabled; + + /////////////////////////////////////////////////////////////////////////// + // Classes: + + /** + * Class instance builder, after the builder pattern. This allows each + * {@code BOSHClientConfig} instance to be immutable while providing + * flexibility when building new {@code BOSHClientConfig} instances. + * <p/> + * Instances of this class are <b>not</b> thread-safe. If template-style + * use is desired, see the {@code create(BOSHClientConfig)} method. + */ + public static final class Builder { + // Required args + private final URI bURI; + private final String bDomain; + + // Optional args + private String bFrom; + private String bLang; + private String bRoute; + private String bProxyHost; + private int bProxyPort; + private SSLContext bSSLContext; + private Boolean bCompression; + + /** + * Creates a new builder instance, used to create instances of the + * {@code BOSHClientConfig} class. + * + * @param cmURI URI to use to contact the connection manager + * @param domain target domain to communicate with + */ + private Builder(final URI cmURI, final String domain) { + bURI = cmURI; + bDomain = domain; + } + + /** + * Creates a new builder instance, used to create instances of the + * {@code BOSHClientConfig} class. + * + * @param cmURI URI to use to contact the connection manager + * @param domain target domain to communicate with + * @return builder instance + */ + public static Builder create(final URI cmURI, final String domain) { + if (cmURI == null) { + throw(new IllegalArgumentException( + "Connection manager URI must not be null")); + } + if (domain == null) { + throw(new IllegalArgumentException( + "Target domain must not be null")); + } + String scheme = cmURI.getScheme(); + if (!("http".equals(scheme) || "https".equals(scheme))) { + throw(new IllegalArgumentException( + "Only 'http' and 'https' URI are allowed")); + } + return new Builder(cmURI, domain); + } + + /** + * Creates a new builder instance using the existing configuration + * provided as a starting point. + * + * @param cfg configuration to copy + * @return builder instance + */ + public static Builder create(final BOSHClientConfig cfg) { + Builder result = new Builder(cfg.getURI(), cfg.getTo()); + result.bFrom = cfg.getFrom(); + result.bLang = cfg.getLang(); + result.bRoute = cfg.getRoute(); + result.bProxyHost = cfg.getProxyHost(); + result.bProxyPort = cfg.getProxyPort(); + result.bSSLContext = cfg.getSSLContext(); + result.bCompression = cfg.isCompressionEnabled(); + return result; + } + + /** + * Set the ID of the client station, to be forwarded to the connection + * manager when new sessions are created. + * + * @param id client ID + * @return builder instance + */ + public Builder setFrom(final String id) { + if (id == null) { + throw(new IllegalArgumentException( + "Client ID must not be null")); + } + bFrom = id; + return this; + } + + /** + * Set the default language of any human-readable content within the + * XML. + * + * @param lang XML language ID + * @return builder instance + */ + public Builder setXMLLang(final String lang) { + if (lang == null) { + throw(new IllegalArgumentException( + "Default language ID must not be null")); + } + bLang = lang; + return this; + } + + /** + * Sets the destination server/domain that the client should connect to. + * Connection managers may be configured to enable sessions with more + * that one server in different domains. When requesting a session with + * such a "proxy" connection manager, a client should use this method to + * specify the server with which it wants to communicate. + * + * @param protocol connection protocol (e.g, "xmpp") + * @param host host or domain to be served by the remote server. Note + * that this is not necessarily the host name or domain name of the + * remote server. + * @param port port number of the remote server + * @return builder instance + */ + public Builder setRoute( + final String protocol, + final String host, + final int port) { + if (protocol == null) { + throw(new IllegalArgumentException("Protocol cannot be null")); + } + if (protocol.contains(":")) { + throw(new IllegalArgumentException( + "Protocol cannot contain the ':' character")); + } + if (host == null) { + throw(new IllegalArgumentException("Host cannot be null")); + } + if (host.contains(":")) { + throw(new IllegalArgumentException( + "Host cannot contain the ':' character")); + } + if (port <= 0) { + throw(new IllegalArgumentException("Port number must be > 0")); + } + bRoute = protocol + ":" + host + ":" + port; + return this; + } + + /** + * Specify the hostname and port of an HTTP proxy to connect through. + * + * @param hostName proxy hostname + * @param port proxy port number + * @return builder instance + */ + public Builder setProxy(final String hostName, final int port) { + if (hostName == null || hostName.length() == 0) { + throw(new IllegalArgumentException( + "Proxy host name cannot be null or empty")); + } + if (port <= 0) { + throw(new IllegalArgumentException( + "Proxy port must be > 0")); + } + bProxyHost = hostName; + bProxyPort = port; + return this; + } + + /** + * Set the SSL context to use for this session. This can be used + * to configure certificate-based authentication, etc.. + * + * @param ctx SSL context + * @return builder instance + */ + public Builder setSSLContext(final SSLContext ctx) { + if (ctx == null) { + throw(new IllegalArgumentException( + "SSL context cannot be null")); + } + bSSLContext = ctx; + return this; + } + + /** + * Set whether or not compression of the underlying data stream + * should be attempted. By default, compression is disabled. + * + * @param enabled set to {@code true} if compression should be + * attempted when possible, {@code false} to disable compression + * @return builder instance + */ + public Builder setCompressionEnabled(final boolean enabled) { + bCompression = Boolean.valueOf(enabled); + return this; + } + + /** + * Build the immutable object instance with the current configuration. + * + * @return BOSHClientConfig instance + */ + public BOSHClientConfig build() { + // Default XML language + String lang; + if (bLang == null) { + lang = "en"; + } else { + lang = bLang; + } + + // Default proxy port + int port; + if (bProxyHost == null) { + port = 0; + } else { + port = bProxyPort; + } + + // Default compression + boolean compression; + if (bCompression == null) { + compression = false; + } else { + compression = bCompression.booleanValue(); + } + + return new BOSHClientConfig( + bURI, + bDomain, + bFrom, + lang, + bRoute, + bProxyHost, + port, + bSSLContext, + compression); + } + + } + + /////////////////////////////////////////////////////////////////////////// + // Constructor: + + /** + * Prevent direct construction. + * + * @param cURI URI of the connection manager to connect to + * @param cDomain the target domain of the first stream + * @param cFrom client ID + * @param cLang default XML language + * @param cRoute target route + * @param cProxyHost proxy host + * @param cProxyPort proxy port + * @param cSSLContext SSL context + * @param cCompression compression enabled flag + */ + private BOSHClientConfig( + final URI cURI, + final String cDomain, + final String cFrom, + final String cLang, + final String cRoute, + final String cProxyHost, + final int cProxyPort, + final SSLContext cSSLContext, + final boolean cCompression) { + uri = cURI; + to = cDomain; + from = cFrom; + lang = cLang; + route = cRoute; + proxyHost = cProxyHost; + proxyPort = cProxyPort; + sslContext = cSSLContext; + compressionEnabled = cCompression; + } + + /** + * Get the URI to use to contact the connection manager. + * + * @return connection manager URI. + */ + public URI getURI() { + return uri; + } + + /** + * Get the ID of the target domain. + * + * @return domain id + */ + public String getTo() { + return to; + } + + /** + * Get the ID of the local client. + * + * @return client id, or {@code null} + */ + public String getFrom() { + return from; + } + + /** + * Get the default language of any human-readable content within the + * XML. Defaults to "en". + * + * @return XML language ID + */ + public String getLang() { + return lang; + } + + /** + * Get the routing information for messages sent to the CM. + * + * @return route attribute string, or {@code null} if no routing + * info was provided. + */ + public String getRoute() { + return route; + } + + /** + * Get the HTTP proxy host to use. + * + * @return proxy host, or {@code null} if no proxy information was specified + */ + public String getProxyHost() { + return proxyHost; + } + + /** + * Get the HTTP proxy port to use. + * + * @return proxy port, or 0 if no proxy information was specified + */ + public int getProxyPort() { + return proxyPort; + } + + /** + * Get the SSL context to use for this session. + * + * @return SSL context instance to use, or {@code null} if no + * context instance was provided. + */ + public SSLContext getSSLContext() { + return sslContext; + } + + /** + * Determines whether or not compression of the underlying data stream + * should be attempted/allowed. Defaults to {@code false}. + * + * @return {@code true} if compression should be attempted, {@code false} + * if compression is disabled or was not specified + */ + boolean isCompressionEnabled() { + return compressionEnabled; + } + +} diff --git a/src/com/kenai/jbosh/BOSHClientConnEvent.java b/src/com/kenai/jbosh/BOSHClientConnEvent.java new file mode 100644 index 0000000..0ac7943 --- /dev/null +++ b/src/com/kenai/jbosh/BOSHClientConnEvent.java @@ -0,0 +1,189 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EventObject; +import java.util.List; + +/** + * Client connection event, notifying of changes in connection state. + * <p/> + * This class is immutable and thread-safe. + */ +public final class BOSHClientConnEvent extends EventObject { + + /** + * Serialized version. + */ + private static final long serialVersionUID = 1L; + + /** + * Boolean flag indicating whether or not a session has been established + * and is currently active. + */ + private final boolean connected; + + /** + * List of outstanding requests which may not have been sent and/or + * acknowledged by the remote CM. + */ + private final List<ComposableBody> requests; + + /** + * Cause of the session termination, or {@code null}. + */ + private final Throwable cause; + + /** + * Creates a new connection event instance. + * + * @param source event source + * @param cConnected flag indicating whether or not the session is + * currently active + * @param cRequests outstanding requests when an error condition is + * detected, or {@code null} when not an error condition + * @param cCause cause of the error condition, or {@code null} when no + * error condition is present + */ + private BOSHClientConnEvent( + final BOSHClient source, + final boolean cConnected, + final List<ComposableBody> cRequests, + final Throwable cCause) { + super(source); + connected = cConnected; + cause = cCause; + + if (connected) { + if (cCause != null) { + throw(new IllegalStateException( + "Cannot be connected and have a cause")); + } + if (cRequests != null && cRequests.size() > 0) { + throw(new IllegalStateException( + "Cannot be connected and have outstanding requests")); + } + } + + if (cRequests == null) { + requests = Collections.emptyList(); + } else { + // Defensive copy: + requests = Collections.unmodifiableList( + new ArrayList<ComposableBody>(cRequests)); + } + } + + /** + * Creates a new connection establishment event. + * + * @param source client which has become connected + * @return event instance + */ + static BOSHClientConnEvent createConnectionEstablishedEvent( + final BOSHClient source) { + return new BOSHClientConnEvent(source, true, null, null); + } + + /** + * Creates a new successful connection closed event. This represents + * a clean termination of the client session. + * + * @param source client which has been disconnected + * @return event instance + */ + static BOSHClientConnEvent createConnectionClosedEvent( + final BOSHClient source) { + return new BOSHClientConnEvent(source, false, null, null); + } + + /** + * Creates a connection closed on error event. This represents + * an unexpected termination of the client session. + * + * @param source client which has been disconnected + * @param outstanding list of requests which may not have been received + * by the remote connection manager + * @param cause cause of termination + * @return event instance + */ + static BOSHClientConnEvent createConnectionClosedOnErrorEvent( + final BOSHClient source, + final List<ComposableBody> outstanding, + final Throwable cause) { + return new BOSHClientConnEvent(source, false, outstanding, cause); + } + + /** + * Gets the client from which this event originated. + * + * @return client instance + */ + public BOSHClient getBOSHClient() { + return (BOSHClient) getSource(); + } + + /** + * Returns whether or not the session has been successfully established + * and is currently active. + * + * @return {@code true} if a session is active, {@code false} otherwise + */ + public boolean isConnected() { + return connected; + } + + /** + * Returns whether or not this event indicates an error condition. This + * will never return {@code true} when {@code isConnected()} returns + * {@code true}. + * + * @return {@code true} if the event indicates a terminal error has + * occurred, {@code false} otherwise. + */ + public boolean isError() { + return cause != null; + } + + /** + * Returns the underlying cause of the error condition. This method is + * guaranteed to return {@code null} when @{code isError()} returns + * {@code false}. Similarly, this method is guaranteed to return + * non-@{code null} if {@code isError()} returns {@code true}. + * + * @return underlying cause of the error condition, or {@code null} if + * this event does not represent an error condition + */ + public Throwable getCause() { + return cause; + } + + /** + * Get the list of requests which may not have been sent or were not + * acknowledged by the remote connection manager prior to session + * termination. + * + * @return list of messages which may not have been received by the remote + * connection manager, or an empty list if the session is still connected + */ + public List<ComposableBody> getOutstandingRequests() { + return requests; + } + +} diff --git a/src/com/kenai/jbosh/BOSHClientConnListener.java b/src/com/kenai/jbosh/BOSHClientConnListener.java new file mode 100644 index 0000000..6d646cb --- /dev/null +++ b/src/com/kenai/jbosh/BOSHClientConnListener.java @@ -0,0 +1,34 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +/** + * Interface used by parties interested in monitoring the connection state + * of a client session. + */ +public interface BOSHClientConnListener { + + /** + * Called when the connection state of the client which the listener + * is registered against has changed. The event object supplied can + * be used to determine the current session state. + * + * @param connEvent connection event describing the state + */ + void connectionEvent(BOSHClientConnEvent connEvent); + +} diff --git a/src/com/kenai/jbosh/BOSHClientRequestListener.java b/src/com/kenai/jbosh/BOSHClientRequestListener.java new file mode 100644 index 0000000..2cc92f3 --- /dev/null +++ b/src/com/kenai/jbosh/BOSHClientRequestListener.java @@ -0,0 +1,45 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +/** + * Interface used by parties interested in monitoring outbound requests made + * by the client to the connection manager (CM). No opportunity is provided + * to manipulate the outbound request. + * <p/> + * The messages being sent are typically modified copies of the message + * body provided to the {@code BOSHClient} instance, built from the + * originally provided message body plus additional BOSH protocol + * state and information. Messages may also be sent automatically when the + * protocol requires it, such as maintaining a minimum number of open + * connections to the connection manager. + * <p/> + * Listeners are executed by the sending thread immediately prior to + * message transmission and should not block for any significant amount + * of time. + */ +public interface BOSHClientRequestListener { + + /** + * Called when the listener is being notified that a client request is + * about to be sent to the connection manager. + * + * @param event event instance containing the message being sent + */ + void requestSent(BOSHMessageEvent event); + +} diff --git a/src/com/kenai/jbosh/BOSHClientResponseListener.java b/src/com/kenai/jbosh/BOSHClientResponseListener.java new file mode 100644 index 0000000..1d86e4f --- /dev/null +++ b/src/com/kenai/jbosh/BOSHClientResponseListener.java @@ -0,0 +1,37 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +/** + * Interface used by parties interested in monitoring inbound responses + * to the client from the connection manager (CM). No opportunity is provided + * to manipulate the response. + * <p/> + * Listeners are executed by the message processing thread and should not + * block for any significant amount of time. + */ +public interface BOSHClientResponseListener { + + /** + * Called when the listener is being notified that a response has been + * received from the connection manager. + * + * @param event event instance containing the message being sent + */ + void responseReceived(BOSHMessageEvent event); + +} diff --git a/src/com/kenai/jbosh/BOSHException.java b/src/com/kenai/jbosh/BOSHException.java new file mode 100644 index 0000000..e0bc05b --- /dev/null +++ b/src/com/kenai/jbosh/BOSHException.java @@ -0,0 +1,50 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +/** + * Exception class used by the BOSH API to minimize the number of checked + * exceptions which must be handled by the user of the API. + */ +public class BOSHException extends Exception { + + /** + * Servial version UID. + */ + private static final long serialVersionUID = 1L; + + /** + * Creates a new exception isntance with the specified descriptive message. + * + * @param msg description of the exceptional condition + */ + public BOSHException(final String msg) { + super(msg); + } + + /** + * Creates a new exception isntance with the specified descriptive + * message and the underlying root cause of the exceptional condition. + * + * @param msg description of the exceptional condition + * @param cause root cause or instigator of the condition + */ + public BOSHException(final String msg, final Throwable cause) { + super(msg, cause); + } + +} diff --git a/src/com/kenai/jbosh/BOSHMessageEvent.java b/src/com/kenai/jbosh/BOSHMessageEvent.java new file mode 100644 index 0000000..550903e --- /dev/null +++ b/src/com/kenai/jbosh/BOSHMessageEvent.java @@ -0,0 +1,92 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +import java.util.EventObject; + +/** + * Event representing a message sent to or from a BOSH connection manager. + * <p/> + * This class is immutable and thread-safe. + */ +public final class BOSHMessageEvent extends EventObject { + + /** + * Serialized version. + */ + private static final long serialVersionUID = 1L; + + /** + * Message which was sent or received. + */ + private final AbstractBody body; + + /** + * Creates a new message event instance. + * + * @param source event source + * @param cBody message body + */ + private BOSHMessageEvent( + final Object source, + final AbstractBody cBody) { + super(source); + if (cBody == null) { + throw(new IllegalArgumentException( + "message body may not be null")); + } + body = cBody; + } + + /** + * Creates a new message event for clients sending events to the + * connection manager. + * + * @param source sender of the message + * @param body message body + * @return event instance + */ + static BOSHMessageEvent createRequestSentEvent( + final BOSHClient source, + final AbstractBody body) { + return new BOSHMessageEvent(source, body); + } + + /** + * Creates a new message event for clients receiving new messages + * from the connection manager. + * + * @param source receiver of the message + * @param body message body + * @return event instance + */ + static BOSHMessageEvent createResponseReceivedEvent( + final BOSHClient source, + final AbstractBody body) { + return new BOSHMessageEvent(source, body); + } + + /** + * Gets the message body which was sent or received. + * + * @return message body + */ + public AbstractBody getBody() { + return body; + } + +} diff --git a/src/com/kenai/jbosh/BodyParser.java b/src/com/kenai/jbosh/BodyParser.java new file mode 100644 index 0000000..5ef5276 --- /dev/null +++ b/src/com/kenai/jbosh/BodyParser.java @@ -0,0 +1,36 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +/** + * Interface for parser implementations to implement in order to abstract the + * business of XML parsing out of the Body class. This allows us to leverage + * a variety of parser implementations to gain performance advantages. + */ +interface BodyParser { + + /** + * Parses the XML message, extracting the useful data from the initial + * body element and returning it in a results object. + * + * @param xml XML to parse + * @return useful data parsed out of the XML + * @throws BOSHException on parse error + */ + BodyParserResults parse(String xml) throws BOSHException; + +} diff --git a/src/com/kenai/jbosh/BodyParserResults.java b/src/com/kenai/jbosh/BodyParserResults.java new file mode 100644 index 0000000..955e4bf --- /dev/null +++ b/src/com/kenai/jbosh/BodyParserResults.java @@ -0,0 +1,64 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +import java.util.HashMap; +import java.util.Map; + +/** + * Data extracted from a raw XML message by a BodyParser implementation. + * Currently, this is limited to the attributes of the wrapper element. + */ +final class BodyParserResults { + + /** + * Map of qualified names to their values. This map is defined to + * match the requirement of the {@code Body} class to prevent + * excessive copying. + */ + private final Map<BodyQName, String> attrs = + new HashMap<BodyQName, String>(); + + /** + * Constructor. + */ + BodyParserResults() { + // Empty + } + + /** + * Add an attribute definition to the results. + * + * @param name attribute's qualified name + * @param value attribute value + */ + void addBodyAttributeValue( + final BodyQName name, + final String value) { + attrs.put(name, value); + } + + /** + * Returns the map of attributes added by the parser. + * + * @return map of atributes. Note: This is the live instance, not a copy. + */ + Map<BodyQName, String> getAttributes() { + return attrs; + } + +} diff --git a/src/com/kenai/jbosh/BodyParserSAX.java b/src/com/kenai/jbosh/BodyParserSAX.java new file mode 100644 index 0000000..54c6c01 --- /dev/null +++ b/src/com/kenai/jbosh/BodyParserSAX.java @@ -0,0 +1,206 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.ref.SoftReference; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +/** + * Implementation of the BodyParser interface which uses the SAX API + * that is part of the JDK. Due to the fact that we can cache and reuse + * SAXPArser instances, this has proven to be significantly faster than the + * use of the javax.xml.stream API introduced in Java 6 while simultaneously + * providing an implementation accessible to Java 5 users. + */ +final class BodyParserSAX implements BodyParser { + + /** + * Logger. + */ + private static final Logger LOG = + Logger.getLogger(BodyParserSAX.class.getName()); + + /** + * SAX parser factory. + */ + private static final SAXParserFactory SAX_FACTORY; + static { + SAX_FACTORY = SAXParserFactory.newInstance(); + SAX_FACTORY.setNamespaceAware(true); + SAX_FACTORY.setValidating(false); + } + + /** + * Thread local to contain a SAX parser instance for each thread that + * attempts to use one. This allows us to gain an order of magnitude of + * performance as a result of not constructing parsers for each + * invocation while retaining thread safety. + */ + private static final ThreadLocal<SoftReference<SAXParser>> PARSER = + new ThreadLocal<SoftReference<SAXParser>>() { + @Override protected SoftReference<SAXParser> initialValue() { + return new SoftReference<SAXParser>(null); + } + }; + + /** + * SAX event handler class. + */ + private static final class Handler extends DefaultHandler { + private final BodyParserResults result; + private final SAXParser parser; + private String defaultNS = null; + + private Handler(SAXParser theParser, BodyParserResults results) { + parser = theParser; + result = results; + } + + /** + * {@inheritDoc} + */ + @Override + public void startElement( + final String uri, + final String localName, + final String qName, + final Attributes attributes) { + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest("Start element: " + qName); + LOG.finest(" URI: " + uri); + LOG.finest(" local: " + localName); + } + + BodyQName bodyName = AbstractBody.getBodyQName(); + // Make sure the first element is correct + if (!(bodyName.getNamespaceURI().equals(uri) + && bodyName.getLocalPart().equals(localName))) { + throw(new IllegalStateException( + "Root element was not '" + bodyName.getLocalPart() + + "' in the '" + bodyName.getNamespaceURI() + + "' namespace. (Was '" + localName + "' in '" + uri + + "')")); + } + + // Read in the attributes, making sure to expand the namespaces + // as needed. + for (int idx=0; idx < attributes.getLength(); idx++) { + String attrURI = attributes.getURI(idx); + if (attrURI.length() == 0) { + attrURI = defaultNS; + } + String attrLN = attributes.getLocalName(idx); + String attrVal = attributes.getValue(idx); + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest(" Attribute: {" + attrURI + "}" + + attrLN + " = '" + attrVal + "'"); + } + + BodyQName aqn = BodyQName.create(attrURI, attrLN); + result.addBodyAttributeValue(aqn, attrVal); + } + + parser.reset(); + } + + /** + * {@inheritDoc} + * + * This implementation uses this event hook to keep track of the + * default namespace on the body element. + */ + @Override + public void startPrefixMapping( + final String prefix, + final String uri) { + if (prefix.length() == 0) { + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest("Prefix mapping: <DEFAULT> => " + uri); + } + defaultNS = uri; + } else { + if (LOG.isLoggable(Level.FINEST)) { + LOG.info("Prefix mapping: " + prefix + " => " + uri); + } + } + } + } + + /////////////////////////////////////////////////////////////////////////// + // BodyParser interface methods: + + /** + * {@inheritDoc} + */ + public BodyParserResults parse(String xml) throws BOSHException { + BodyParserResults result = new BodyParserResults(); + Exception thrown; + try { + InputStream inStream = new ByteArrayInputStream(xml.getBytes()); + SAXParser parser = getSAXParser(); + parser.parse(inStream, new Handler(parser, result)); + return result; + } catch (SAXException saxx) { + thrown = saxx; + } catch (IOException iox) { + thrown = iox; + } + throw(new BOSHException("Could not parse body:\n" + xml, thrown)); + } + + /////////////////////////////////////////////////////////////////////////// + // Private methods: + + /** + * Gets a SAXParser for use in parsing incoming messages. + * + * @return parser instance + */ + private static SAXParser getSAXParser() { + SoftReference<SAXParser> ref = PARSER.get(); + SAXParser result = ref.get(); + if (result == null) { + Exception thrown; + try { + result = SAX_FACTORY.newSAXParser(); + ref = new SoftReference<SAXParser>(result); + PARSER.set(ref); + return result; + } catch (ParserConfigurationException ex) { + thrown = ex; + } catch (SAXException ex) { + thrown = ex; + } + throw(new IllegalStateException( + "Could not create SAX parser", thrown)); + } else { + result.reset(); + return result; + } + } + +} diff --git a/src/com/kenai/jbosh/BodyParserXmlPull.java b/src/com/kenai/jbosh/BodyParserXmlPull.java new file mode 100644 index 0000000..5f23b06 --- /dev/null +++ b/src/com/kenai/jbosh/BodyParserXmlPull.java @@ -0,0 +1,165 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +import java.io.IOException; +import java.io.StringReader; +import java.lang.ref.SoftReference; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.xml.XMLConstants; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +/** + * Implementation of the BodyParser interface which uses the XmlPullParser + * API. When available, this API provides an order of magnitude performance + * improvement over the default SAX parser implementation. + */ +final class BodyParserXmlPull implements BodyParser { + + /** + * Logger. + */ + private static final Logger LOG = + Logger.getLogger(BodyParserXmlPull.class.getName()); + + /** + * Thread local to contain a XmlPullParser instance for each thread that + * attempts to use one. This allows us to gain an order of magnitude of + * performance as a result of not constructing parsers for each + * invocation while retaining thread safety. + */ + private static final ThreadLocal<SoftReference<XmlPullParser>> XPP_PARSER = + new ThreadLocal<SoftReference<XmlPullParser>>() { + @Override protected SoftReference<XmlPullParser> initialValue() { + return new SoftReference<XmlPullParser>(null); + } + }; + + /////////////////////////////////////////////////////////////////////////// + // BodyParser interface methods: + + /** + * {@inheritDoc} + */ + public BodyParserResults parse(final String xml) throws BOSHException { + BodyParserResults result = new BodyParserResults(); + Exception thrown; + try { + XmlPullParser xpp = getXmlPullParser(); + + xpp.setInput(new StringReader(xml)); + int eventType = xpp.getEventType(); + while (eventType != XmlPullParser.END_DOCUMENT) { + if (eventType == XmlPullParser.START_TAG) { + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest("Start tag: " + xpp.getName()); + } + } else { + eventType = xpp.next(); + continue; + } + + String prefix = xpp.getPrefix(); + if (prefix == null) { + prefix = XMLConstants.DEFAULT_NS_PREFIX; + } + String uri = xpp.getNamespace(); + String localName = xpp.getName(); + QName name = new QName(uri, localName, prefix); + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest("Start element: "); + LOG.finest(" prefix: " + prefix); + LOG.finest(" URI: " + uri); + LOG.finest(" local: " + localName); + } + + BodyQName bodyName = AbstractBody.getBodyQName(); + if (!bodyName.equalsQName(name)) { + throw(new IllegalStateException( + "Root element was not '" + bodyName.getLocalPart() + + "' in the '" + bodyName.getNamespaceURI() + + "' namespace. (Was '" + localName + + "' in '" + uri + "')")); + } + + for (int idx=0; idx < xpp.getAttributeCount(); idx++) { + String attrURI = xpp.getAttributeNamespace(idx); + if (attrURI.length() == 0) { + attrURI = xpp.getNamespace(null); + } + String attrPrefix = xpp.getAttributePrefix(idx); + if (attrPrefix == null) { + attrPrefix = XMLConstants.DEFAULT_NS_PREFIX; + } + String attrLN = xpp.getAttributeName(idx); + String attrVal = xpp.getAttributeValue(idx); + BodyQName aqn = BodyQName.createWithPrefix( + attrURI, attrLN, attrPrefix); + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest(" Attribute: {" + attrURI + "}" + + attrLN + " = '" + attrVal + "'"); + } + result.addBodyAttributeValue(aqn, attrVal); + } + break; + } + return result; + } catch (RuntimeException rtx) { + thrown = rtx; + } catch (XmlPullParserException xmlppx) { + thrown = xmlppx; + } catch (IOException iox) { + thrown = iox; + } + throw(new BOSHException("Could not parse body:\n" + xml, thrown)); + } + + /////////////////////////////////////////////////////////////////////////// + // Private methods: + + /** + * Gets a XmlPullParser for use in parsing incoming messages. + * + * @return parser instance + */ + private static XmlPullParser getXmlPullParser() { + SoftReference<XmlPullParser> ref = XPP_PARSER.get(); + XmlPullParser result = ref.get(); + if (result == null) { + Exception thrown; + try { + XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); + factory.setNamespaceAware(true); + factory.setValidating(false); + result = factory.newPullParser(); + ref = new SoftReference<XmlPullParser>(result); + XPP_PARSER.set(ref); + return result; + } catch (Exception ex) { + thrown = ex; + } + throw(new IllegalStateException( + "Could not create XmlPull parser", thrown)); + } else { + return result; + } + } + +} diff --git a/src/com/kenai/jbosh/BodyQName.java b/src/com/kenai/jbosh/BodyQName.java new file mode 100644 index 0000000..83acdf1 --- /dev/null +++ b/src/com/kenai/jbosh/BodyQName.java @@ -0,0 +1,165 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +/** + * Qualified name of an attribute of the wrapper element. This class is + * analagous to the {@code javax.xml.namespace.QName} class. + * Each qualified name consists of a namespace URI and a local name. + * <p/> + * Instances of this class are immutable and thread-safe. + */ +public final class BodyQName { + + /** + * BOSH namespace URI. + */ + static final String BOSH_NS_URI = + "http://jabber.org/protocol/httpbind"; + + /** + * Namespace URI. + */ + private final QName qname; + + /** + * Private constructor to prevent direct construction. + * + * @param wrapped QName instance to wrap + */ + private BodyQName( + final QName wrapped) { + qname = wrapped; + } + + /** + * Creates a new qualified name using a namespace URI and local name. + * + * @param uri namespace URI + * @param local local name + * @return BodyQName instance + */ + public static BodyQName create( + final String uri, + final String local) { + return createWithPrefix(uri, local, null); + } + + /** + * Creates a new qualified name using a namespace URI and local name + * along with an optional prefix. + * + * @param uri namespace URI + * @param local local name + * @param prefix optional prefix or @{code null} for no prefix + * @return BodyQName instance + */ + public static BodyQName createWithPrefix( + final String uri, + final String local, + final String prefix) { + if (uri == null || uri.length() == 0) { + throw(new IllegalArgumentException( + "URI is required and may not be null/empty")); + } + if (local == null || local.length() == 0) { + throw(new IllegalArgumentException( + "Local arg is required and may not be null/empty")); + } + if (prefix == null || prefix.length() == 0) { + return new BodyQName(new QName(uri, local)); + } else { + return new BodyQName(new QName(uri, local, prefix)); + } + } + + /** + * Get the namespace URI of this qualified name. + * + * @return namespace uri + */ + public String getNamespaceURI() { + return qname.getNamespaceURI(); + } + + /** + * Get the local part of this qualified name. + * + * @return local name + */ + public String getLocalPart() { + return qname.getLocalPart(); + } + + /** + * Get the optional prefix used with this qualified name, or {@code null} + * if no prefix has been assiciated. + * + * @return prefix, or {@code null} if no prefix was supplied + */ + public String getPrefix() { + return qname.getPrefix(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(final Object obj) { + if (obj instanceof BodyQName) { + BodyQName other = (BodyQName) obj; + return qname.equals(other.qname); + } else { + return false; + } + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return qname.hashCode(); + } + + /////////////////////////////////////////////////////////////////////////// + // Package-private methods: + + /** + * Creates a new qualified name using the BOSH namespace URI and local name. + * + * @param local local name + * @return BodyQName instance + */ + static BodyQName createBOSH( + final String local) { + return createWithPrefix(BOSH_NS_URI, local, null); + } + + /** + * Convenience method to compare this qualified name with a + * {@code javax.xml.namespace.QName}. + * + * @param otherName QName to compare to + * @return @{code true} if the qualified name is the same, {@code false} + * otherwise + */ + boolean equalsQName(final QName otherName) { + return qname.equals(otherName); + } + +} diff --git a/src/com/kenai/jbosh/CMSessionParams.java b/src/com/kenai/jbosh/CMSessionParams.java new file mode 100644 index 0000000..bbed628 --- /dev/null +++ b/src/com/kenai/jbosh/CMSessionParams.java @@ -0,0 +1,177 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +/** + * A BOSH connection manager session instance. This consolidates the + * configuration knowledge related to the CM session and provides a + * mechanism by which + */ +final class CMSessionParams { + + private final AttrSessionID sid; + + private final AttrWait wait; + + private final AttrVersion ver; + + private final AttrPolling polling; + + private final AttrInactivity inactivity; + + private final AttrRequests requests; + + private final AttrHold hold; + + private final AttrAccept accept; + + private final AttrMaxPause maxPause; + + private final AttrAck ack; + + private final AttrCharsets charsets; + + private final boolean ackingRequests; + + /** + * Prevent direct construction. + */ + private CMSessionParams( + final AttrSessionID aSid, + final AttrWait aWait, + final AttrVersion aVer, + final AttrPolling aPolling, + final AttrInactivity aInactivity, + final AttrRequests aRequests, + final AttrHold aHold, + final AttrAccept aAccept, + final AttrMaxPause aMaxPause, + final AttrAck aAck, + final AttrCharsets aCharsets, + final boolean amAckingRequests) { + sid = aSid; + wait = aWait; + ver = aVer; + polling = aPolling; + inactivity = aInactivity; + requests = aRequests; + hold = aHold; + accept = aAccept; + maxPause = aMaxPause; + ack = aAck; + charsets = aCharsets; + ackingRequests = amAckingRequests; + } + + static CMSessionParams fromSessionInit( + final AbstractBody req, + final AbstractBody resp) + throws BOSHException { + AttrAck aAck = AttrAck.createFromString( + resp.getAttribute(Attributes.ACK)); + String rid = req.getAttribute(Attributes.RID); + boolean acking = (aAck != null && aAck.getValue().equals(rid)); + + return new CMSessionParams( + AttrSessionID.createFromString( + getRequiredAttribute(resp, Attributes.SID)), + AttrWait.createFromString( + getRequiredAttribute(resp, Attributes.WAIT)), + AttrVersion.createFromString( + resp.getAttribute(Attributes.VER)), + AttrPolling.createFromString( + resp.getAttribute(Attributes.POLLING)), + AttrInactivity.createFromString( + resp.getAttribute(Attributes.INACTIVITY)), + AttrRequests.createFromString( + resp.getAttribute(Attributes.REQUESTS)), + AttrHold.createFromString( + resp.getAttribute(Attributes.HOLD)), + AttrAccept.createFromString( + resp.getAttribute(Attributes.ACCEPT)), + AttrMaxPause.createFromString( + resp.getAttribute(Attributes.MAXPAUSE)), + aAck, + AttrCharsets.createFromString( + resp.getAttribute(Attributes.CHARSETS)), + acking + ); + } + + private static String getRequiredAttribute( + final AbstractBody body, + final BodyQName name) + throws BOSHException { + String attrStr = body.getAttribute(name); + if (attrStr == null) { + throw(new BOSHException( + "Connection Manager session creation response did not " + + "include required '" + name.getLocalPart() + + "' attribute")); + } + return attrStr; + } + + AttrSessionID getSessionID() { + return sid; + } + + AttrWait getWait() { + return wait; + } + + AttrVersion getVersion() { + return ver; + } + + AttrPolling getPollingInterval() { + return polling; + } + + AttrInactivity getInactivityPeriod() { + return inactivity; + } + + AttrRequests getRequests() { + return requests; + } + + AttrHold getHold() { + return hold; + } + + AttrAccept getAccept() { + return accept; + } + + AttrMaxPause getMaxPause() { + return maxPause; + } + + AttrAck getAck() { + return ack; + } + + AttrCharsets getCharsets() { + return charsets; + } + + boolean isAckingRequests() { + return ackingRequests; + } + +} diff --git a/src/com/kenai/jbosh/ComposableBody.java b/src/com/kenai/jbosh/ComposableBody.java new file mode 100644 index 0000000..d375478 --- /dev/null +++ b/src/com/kenai/jbosh/ComposableBody.java @@ -0,0 +1,345 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.xml.XMLConstants; + +/** + * Implementation of the {@code AbstractBody} class which allows for the + * definition of messages from individual elements of a body. + * <p/> + * A message is constructed by creating a builder, manipulating the + * configuration of the builder, and then building it into a class instance, + * as in the following example: + * <pre> + * ComposableBody body = ComposableBody.builder() + * .setNamespaceDefinition("foo", "http://foo.com/bar") + * .setPayloadXML("<foo:data>Data to send to remote server</foo:data>") + * .build(); + * </pre> + * Class instances can also be "rebuilt", allowing them to be used as templates + * when building many similar messages: + * <pre> + * ComposableBody body2 = body.rebuild() + * .setPayloadXML("<foo:data>More data to send</foo:data>") + * .build(); + * </pre> + * This class does only minimal syntactic and semantic checking with respect + * to what the generated XML will look like. It is up to the developer to + * protect against the definition of malformed XML messages when building + * instances of this class. + * <p/> + * Instances of this class are immutable and thread-safe. + */ +public final class ComposableBody extends AbstractBody { + + /** + * Pattern used to identify the beginning {@code body} element of a + * BOSH message. + */ + private static final Pattern BOSH_START = + Pattern.compile("<" + "(?:(?:[^:\t\n\r >]+:)|(?:\\{[^\\}>]*?}))?" + + "body" + "(?:[\t\n\r ][^>]*?)?" + "(/>|>)"); + + /** + * Map of all attributes to their values. + */ + private final Map<BodyQName, String> attrs; + + /** + * Payload XML. + */ + private final String payload; + + /** + * Computed raw XML. + */ + private final AtomicReference<String> computed = + new AtomicReference<String>(); + + /** + * Class instance builder, after the builder pattern. This allows each + * message instance to be immutable while providing flexibility when + * building new messages. + * <p/> + * Instances of this class are <b>not</b> thread-safe. + */ + public static final class Builder { + private Map<BodyQName, String> map; + private boolean doMapCopy; + private String payloadXML; + + /** + * Prevent direct construction. + */ + private Builder() { + // Empty + } + + /** + * Creates a builder which is initialized to the values of the + * provided {@code ComposableBody} instance. This allows an + * existing {@code ComposableBody} to be used as a + * template/starting point. + * + * @param source body template + * @return builder instance + */ + private static Builder fromBody(final ComposableBody source) { + Builder result = new Builder(); + result.map = source.getAttributes(); + result.doMapCopy = true; + result.payloadXML = source.payload; + return result; + } + + /** + * Set the body message's wrapped payload content. Any previous + * content will be replaced. + * + * @param xml payload XML content + * @return builder instance + */ + public Builder setPayloadXML(final String xml) { + if (xml == null) { + throw(new IllegalArgumentException( + "payload XML argument cannot be null")); + } + payloadXML = xml; + return this; + } + + /** + * Set an attribute on the message body / wrapper element. + * + * @param name qualified name of the attribute + * @param value value of the attribute + * @return builder instance + */ + public Builder setAttribute( + final BodyQName name, final String value) { + if (map == null) { + map = new HashMap<BodyQName, String>(); + } else if (doMapCopy) { + map = new HashMap<BodyQName, String>(map); + doMapCopy = false; + } + if (value == null) { + map.remove(name); + } else { + map.put(name, value); + } + return this; + } + + /** + * Convenience method to set a namespace definition. This would result + * in a namespace prefix definition similar to: + * {@code <body xmlns:prefix="uri"/>} + * + * @param prefix prefix to define + * @param uri namespace URI to associate with the prefix + * @return builder instance + */ + public Builder setNamespaceDefinition( + final String prefix, final String uri) { + BodyQName qname = BodyQName.createWithPrefix( + XMLConstants.XML_NS_URI, prefix, + XMLConstants.XMLNS_ATTRIBUTE); + return setAttribute(qname, uri); + } + + /** + * Build the immutable object instance with the current configuration. + * + * @return composable body instance + */ + public ComposableBody build() { + if (map == null) { + map = new HashMap<BodyQName, String>(); + } + if (payloadXML == null) { + payloadXML = ""; + } + return new ComposableBody(map, payloadXML); + } + } + + /////////////////////////////////////////////////////////////////////////// + // Constructors: + + /** + * Prevent direct construction. This constructor is for body messages + * which are dynamically assembled. + */ + private ComposableBody( + final Map<BodyQName, String> attrMap, + final String payloadXML) { + super(); + attrs = attrMap; + payload = payloadXML; + } + + /** + * Parse a static body instance into a composable instance. This is an + * expensive operation and should not be used lightly. + * <p/> + * The current implementation does not obtain the payload XML by means of + * a proper XML parser. It uses some string pattern searching to find the + * first @{code body} element and the last element's closing tag. It is + * assumed that the static body's XML is well formed, etc.. This + * implementation may change in the future. + * + * @param body static body instance to convert + * @return composable bosy instance + * @throws BOSHException + */ + static ComposableBody fromStaticBody(final StaticBody body) + throws BOSHException { + String raw = body.toXML(); + Matcher matcher = BOSH_START.matcher(raw); + if (!matcher.find()) { + throw(new BOSHException( + "Could not locate 'body' element in XML. The raw XML did" + + " not match the pattern: " + BOSH_START)); + } + String payload; + if (">".equals(matcher.group(1))) { + int first = matcher.end(); + int last = raw.lastIndexOf("</"); + if (last < first) { + last = first; + } + payload = raw.substring(first, last); + } else { + payload = ""; + } + + return new ComposableBody(body.getAttributes(), payload); + } + + /** + * Create a builder instance to build new instances of this class. + * + * @return AbstractBody instance + */ + public static Builder builder() { + return new Builder(); + } + + /** + * If this {@code ComposableBody} instance is a dynamic instance, uses this + * {@code ComposableBody} instance as a starting point, create a builder + * which can be used to create another {@code ComposableBody} instance + * based on this one. This allows a {@code ComposableBody} instance to be + * used as a template. Note that the use of the returned builder in no + * way modifies or manipulates the current {@code ComposableBody} instance. + * + * @return builder instance which can be used to build similar + * {@code ComposableBody} instances + */ + public Builder rebuild() { + return Builder.fromBody(this); + } + + /////////////////////////////////////////////////////////////////////////// + // Accessors: + + /** + * {@inheritDoc} + */ + public Map<BodyQName, String> getAttributes() { + return Collections.unmodifiableMap(attrs); + } + + /** + * {@inheritDoc} + */ + public String toXML() { + String comp = computed.get(); + if (comp == null) { + comp = computeXML(); + computed.set(comp); + } + return comp; + } + + /** + * Get the paylaod XML in String form. + * + * @return payload XML + */ + public String getPayloadXML() { + return payload; + } + + /////////////////////////////////////////////////////////////////////////// + // Private methods: + + /** + * Escape the value of an attribute to ensure we maintain valid + * XML syntax. + * + * @param value value to escape + * @return escaped value + */ + private String escape(final String value) { + return value.replace("'", "'"); + } + + /** + * Generate a String representation of the message body. + * + * @return XML string representation of the body + */ + private String computeXML() { + BodyQName bodyName = getBodyQName(); + StringBuilder builder = new StringBuilder(); + builder.append("<"); + builder.append(bodyName.getLocalPart()); + for (Map.Entry<BodyQName, String> entry : attrs.entrySet()) { + builder.append(" "); + BodyQName name = entry.getKey(); + String prefix = name.getPrefix(); + if (prefix != null && prefix.length() > 0) { + builder.append(prefix); + builder.append(":"); + } + builder.append(name.getLocalPart()); + builder.append("='"); + builder.append(escape(entry.getValue())); + builder.append("'"); + } + builder.append(" "); + builder.append(XMLConstants.XMLNS_ATTRIBUTE); + builder.append("='"); + builder.append(bodyName.getNamespaceURI()); + builder.append("'>"); + if (payload != null) { + builder.append(payload); + } + builder.append("</body>"); + return builder.toString(); + } + +} diff --git a/src/com/kenai/jbosh/GZIPCodec.java b/src/com/kenai/jbosh/GZIPCodec.java new file mode 100644 index 0000000..988f27f --- /dev/null +++ b/src/com/kenai/jbosh/GZIPCodec.java @@ -0,0 +1,104 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +/** + * Codec methods for compressing and uncompressing using GZIP. + */ +final class GZIPCodec { + + /** + * Size of the internal buffer when decoding. + */ + private static final int BUFFER_SIZE = 512; + + /////////////////////////////////////////////////////////////////////////// + // Constructors: + + /** + * Prevent construction. + */ + private GZIPCodec() { + // Empty + } + + /** + * Returns the name of the codec. + * + * @return string name of the codec (i.e., "gzip") + */ + public static String getID() { + return "gzip"; + } + + /** + * Compress/encode the data provided using the GZIP format. + * + * @param data data to compress + * @return compressed data + * @throws IOException on compression failure + */ + public static byte[] encode(final byte[] data) throws IOException { + ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); + GZIPOutputStream gzOut = null; + try { + gzOut = new GZIPOutputStream(byteOut); + gzOut.write(data); + gzOut.close(); + byteOut.close(); + return byteOut.toByteArray(); + } finally { + gzOut.close(); + byteOut.close(); + } + } + + /** + * Uncompress/decode the data provided using the GZIP format. + * + * @param data data to uncompress + * @return uncompressed data + * @throws IOException on decompression failure + */ + public static byte[] decode(final byte[] compressed) throws IOException { + ByteArrayInputStream byteIn = new ByteArrayInputStream(compressed); + ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); + GZIPInputStream gzIn = null; + try { + gzIn = new GZIPInputStream(byteIn); + int read; + byte[] buffer = new byte[BUFFER_SIZE]; + do { + read = gzIn.read(buffer); + if (read > 0) { + byteOut.write(buffer, 0, read); + } + } while (read >= 0); + return byteOut.toByteArray(); + } finally { + gzIn.close(); + byteOut.close(); + } + } + +} diff --git a/src/com/kenai/jbosh/HTTPExchange.java b/src/com/kenai/jbosh/HTTPExchange.java new file mode 100644 index 0000000..c77caf0 --- /dev/null +++ b/src/com/kenai/jbosh/HTTPExchange.java @@ -0,0 +1,126 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A request and response pair representing a single exchange with a remote + * content manager. This is primarily a container class intended to maintain + * the relationship between the request and response but allows the response + * to be added after the fact. + */ +final class HTTPExchange { + + /** + * Logger. + */ + private static final Logger LOG = + Logger.getLogger(HTTPExchange.class.getName()); + + /** + * Request body. + */ + private final AbstractBody request; + + /** + * Lock instance used to protect and provide conditions. + */ + private final Lock lock = new ReentrantLock(); + + /** + * Condition used to signal when the response has been set. + */ + private final Condition ready = lock.newCondition(); + + /** + * HTTPResponse instance. + */ + private HTTPResponse response; + + /////////////////////////////////////////////////////////////////////////// + // Constructor: + + /** + * Create a new request/response pair object. + * + * @param req request message body + */ + HTTPExchange(final AbstractBody req) { + if (req == null) { + throw(new IllegalArgumentException("Request body cannot be null")); + } + request = req; + } + + /////////////////////////////////////////////////////////////////////////// + // Package-private methods: + + /** + * Get the original request message. + * + * @return request message body. + */ + AbstractBody getRequest() { + return request; + } + + /** + * Set the HTTPResponse instance. + * + * @return HTTPResponse instance associated with the request. + */ + void setHTTPResponse(HTTPResponse resp) { + lock.lock(); + try { + if (response != null) { + throw(new IllegalStateException( + "HTTPResponse was already set")); + } + response = resp; + ready.signalAll(); + } finally { + lock.unlock(); + } + } + + /** + * Get the HTTPResponse instance. + * + * @return HTTPResponse instance associated with the request. + */ + HTTPResponse getHTTPResponse() { + lock.lock(); + try { + while (response == null) { + try { + ready.await(); + } catch (InterruptedException intx) { + LOG.log(Level.FINEST, "Interrupted", intx); + } + } + return response; + } finally { + lock.unlock(); + } + } + +} diff --git a/src/com/kenai/jbosh/HTTPResponse.java b/src/com/kenai/jbosh/HTTPResponse.java new file mode 100644 index 0000000..f1f301c --- /dev/null +++ b/src/com/kenai/jbosh/HTTPResponse.java @@ -0,0 +1,54 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +/** + * This class represents a complete HTTP response to a request made via + * a {@code HTTPSender} send request. Instances of this interface are + * intended to represent a deferred, future response, not necessarily a + * response which is immediately available. + */ +interface HTTPResponse { + + /** + * Close out any resources still held by the original request. The + * conversation may need to be aborted if the session it was a part of + * gets abruptly terminated. + */ + void abort(); + + /** + * Get the HTTP status code of the response (e.g., 200, 404, etc.). If + * the response has not yet been received from the remote server, this + * method should block until the response has arrived. + * + * @return HTTP status code + * @throws InterruptedException if interrupted while awaiting response + */ + int getHTTPStatus() throws InterruptedException, BOSHException; + + /** + * Get the HTTP response message body. If the response has not yet been + * received from the remote server, this method should block until the + * response has arrived. + * + * @return response message body + * @throws InterruptedException if interrupted while awaiting response + */ + AbstractBody getBody() throws InterruptedException, BOSHException; + +} diff --git a/src/com/kenai/jbosh/HTTPSender.java b/src/com/kenai/jbosh/HTTPSender.java new file mode 100644 index 0000000..486d274 --- /dev/null +++ b/src/com/kenai/jbosh/HTTPSender.java @@ -0,0 +1,54 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +/** + * Interface used to represent code which can send a BOSH XML body over + * HTTP to a connection manager. + */ +interface HTTPSender { + + /** + * Initialize the HTTP sender instance for use with the session provided. + * This method will be called once before use of the service instance. + * + * @param sessionCfg session configuration + */ + void init(BOSHClientConfig sessionCfg); + + /** + * Dispose of all resources used to provide the required services. This + * method will be called once when the service instance is no longer + * required. + */ + void destroy(); + + /** + * Create a {@code Callable} instance which can be used to send the + * request specified to the connection manager. This method should + * return immediately, prior to doing any real work. The invocation + * of the returned {@code Callable} should send the request (if it has + * not already been sent by the time of the call), block while waiting + * for the response, and then return the response body. + * + * @param params CM session creation resopnse params + * @param body request body to send + * @return callable used to access the response + */ + HTTPResponse send(CMSessionParams params, AbstractBody body); + +} diff --git a/src/com/kenai/jbosh/QName.java b/src/com/kenai/jbosh/QName.java new file mode 100644 index 0000000..d395a06 --- /dev/null +++ b/src/com/kenai/jbosh/QName.java @@ -0,0 +1,269 @@ +/* + * The Apache Software License, Version 1.1 + * + * + * Copyright (c) 2001-2003 The Apache Software Foundation. All rights + * reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, + * if any, must include the following acknowledgment: + * "This product includes software developed by the + * Apache Software Foundation (http://www.apache.org/)." + * Alternately, this acknowledgment may appear in the software itself, + * if and wherever such third-party acknowledgments normally appear. + * + * 4. The names "Axis" and "Apache Software Foundation" must + * not be used to endorse or promote products derived from this + * software without prior written permission. For written + * permission, please contact apache@apache.org. + * + * 5. Products derived from this software may not be called "Apache", + * nor may "Apache" appear in their name, without prior written + * permission of the Apache Software Foundation. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + */ +package com.kenai.jbosh; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; + +/** + * <code>QName</code> class represents the value of a qualified name + * as specified in <a href="http://www.w3.org/TR/xmlschema-2/#QName">XML + * Schema Part2: Datatypes specification</a>. + * <p> + * The value of a QName contains a <b>namespaceURI</b>, a <b>localPart</b> and a <b>prefix</b>. + * The localPart provides the local part of the qualified name. The + * namespaceURI is a URI reference identifying the namespace. + * + * @version 1.1 + */ +public class QName implements Serializable { + + /** comment/shared empty string */ + private static final String emptyString = "".intern(); + + /** Field namespaceURI */ + private String namespaceURI; + + /** Field localPart */ + private String localPart; + + /** Field prefix */ + private String prefix; + + /** + * Constructor for the QName. + * + * @param localPart Local part of the QName + */ + public QName(String localPart) { + this(emptyString, localPart, emptyString); + } + + /** + * Constructor for the QName. + * + * @param namespaceURI Namespace URI for the QName + * @param localPart Local part of the QName. + */ + public QName(String namespaceURI, String localPart) { + this(namespaceURI, localPart, emptyString); + } + + /** + * Constructor for the QName. + * + * @param namespaceURI Namespace URI for the QName + * @param localPart Local part of the QName. + * @param prefix Prefix of the QName. + */ + public QName(String namespaceURI, String localPart, String prefix) { + this.namespaceURI = (namespaceURI == null) + ? emptyString + : namespaceURI.intern(); + if (localPart == null) { + throw new IllegalArgumentException("invalid QName local part"); + } else { + this.localPart = localPart.intern(); + } + + if (prefix == null) { + throw new IllegalArgumentException("invalid QName prefix"); + } else { + this.prefix = prefix.intern(); + } + } + + /** + * Gets the Namespace URI for this QName + * + * @return Namespace URI + */ + public String getNamespaceURI() { + return namespaceURI; + } + + /** + * Gets the Local part for this QName + * + * @return Local part + */ + public String getLocalPart() { + return localPart; + } + + /** + * Gets the Prefix for this QName + * + * @return Prefix + */ + public String getPrefix() { + return prefix; + } + + /** + * Returns a string representation of this QName + * + * @return a string representation of the QName + */ + public String toString() { + + return ((namespaceURI == emptyString) + ? localPart + : '{' + namespaceURI + '}' + localPart); + } + + /** + * Tests this QName for equality with another object. + * <p> + * If the given object is not a QName or is null then this method + * returns <tt>false</tt>. + * <p> + * For two QNames to be considered equal requires that both + * localPart and namespaceURI must be equal. This method uses + * <code>String.equals</code> to check equality of localPart + * and namespaceURI. Any class that extends QName is required + * to satisfy this equality contract. + * <p> + * This method satisfies the general contract of the <code>Object.equals</code> method. + * + * @param obj the reference object with which to compare + * + * @return <code>true</code> if the given object is identical to this + * QName: <code>false</code> otherwise. + */ + public final boolean equals(Object obj) { + + if (obj == this) { + return true; + } + + if (!(obj instanceof QName)) { + return false; + } + + if ((namespaceURI == ((QName) obj).namespaceURI) + && (localPart == ((QName) obj).localPart)) { + return true; + } + + return false; + } + + /** + * Returns a QName holding the value of the specified String. + * <p> + * The string must be in the form returned by the QName.toString() + * method, i.e. "{namespaceURI}localPart", with the "{namespaceURI}" + * part being optional. + * <p> + * This method doesn't do a full validation of the resulting QName. + * In particular, it doesn't check that the resulting namespace URI + * is a legal URI (per RFC 2396 and RFC 2732), nor that the resulting + * local part is a legal NCName per the XML Namespaces specification. + * + * @param s the string to be parsed + * @throws java.lang.IllegalArgumentException If the specified String cannot be parsed as a QName + * @return QName corresponding to the given String + */ + public static QName valueOf(String s) { + + if ((s == null) || s.equals("")) { + throw new IllegalArgumentException("invalid QName literal"); + } + + if (s.charAt(0) == '{') { + int i = s.indexOf('}'); + + if (i == -1) { + throw new IllegalArgumentException("invalid QName literal"); + } + + if (i == s.length() - 1) { + throw new IllegalArgumentException("invalid QName literal"); + } else { + return new QName(s.substring(1, i), s.substring(i + 1)); + } + } else { + return new QName(s); + } + } + + /** + * Returns a hash code value for this QName object. The hash code + * is based on both the localPart and namespaceURI parts of the + * QName. This method satisfies the general contract of the + * <code>Object.hashCode</code> method. + * + * @return a hash code value for this Qname object + */ + public final int hashCode() { + return namespaceURI.hashCode() ^ localPart.hashCode(); + } + + /** + * Ensure that deserialization properly interns the results. + * @param in the ObjectInputStream to be read + */ + private void readObject(ObjectInputStream in) throws + IOException, ClassNotFoundException { + in.defaultReadObject(); + + namespaceURI = namespaceURI.intern(); + localPart = localPart.intern(); + prefix = prefix.intern(); + } +} + diff --git a/src/com/kenai/jbosh/RequestIDSequence.java b/src/com/kenai/jbosh/RequestIDSequence.java new file mode 100644 index 0000000..14b1475 --- /dev/null +++ b/src/com/kenai/jbosh/RequestIDSequence.java @@ -0,0 +1,120 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +import java.security.SecureRandom; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Request ID sequence generator. This generator generates a random first + * RID and then manages the sequence from there on out. + */ +final class RequestIDSequence { + + /** + * Maximum number of bits available for representing request IDs, according + * to the XEP-0124 spec.s + */ + private static final int MAX_BITS = 53; + + /** + * Bits devoted to incremented values. + */ + private static final int INCREMENT_BITS = 32; + + /** + * Minimum number of times the initial RID can be incremented before + * exceeding the maximum. + */ + private static final long MIN_INCREMENTS = 1L << INCREMENT_BITS; + + /** + * Max initial value. + */ + private static final long MAX_INITIAL = (1L << MAX_BITS) - MIN_INCREMENTS; + + /** + * Max bits mask. + */ + private static final long MASK = ~(Long.MAX_VALUE << MAX_BITS); + + /** + * Random number generator. + */ + private static final SecureRandom RAND = new SecureRandom(); + + /** + * Internal lock. + */ + private static final Lock LOCK = new ReentrantLock(); + + /** + * The last reqest ID used, or <= 0 if a new request ID needs to be + * generated. + */ + private AtomicLong nextRequestID = new AtomicLong(); + + /////////////////////////////////////////////////////////////////////////// + // Constructors: + + /** + * Prevent direct construction. + */ + RequestIDSequence() { + nextRequestID = new AtomicLong(generateInitialValue()); + } + + /////////////////////////////////////////////////////////////////////////// + // Public methods: + + /** + * Calculates the next request ID value to use. This number must be + * initialized such that it is unlikely to ever exceed 2 ^ 53, according + * to XEP-0124. + * + * @return next request ID value + */ + public long getNextRID() { + return nextRequestID.getAndIncrement(); + } + + /////////////////////////////////////////////////////////////////////////// + // Private methods: + + /** + * Generates an initial RID value by generating numbers until a number is + * found which is smaller than the maximum allowed value and greater + * than zero. + * + * @return random initial value + */ + private long generateInitialValue() { + long result; + LOCK.lock(); + try { + do { + result = RAND.nextLong() & MASK; + } while (result > MAX_INITIAL); + } finally { + LOCK.unlock(); + } + return result; + } + +} diff --git a/src/com/kenai/jbosh/ServiceLib.java b/src/com/kenai/jbosh/ServiceLib.java new file mode 100644 index 0000000..07d0556 --- /dev/null +++ b/src/com/kenai/jbosh/ServiceLib.java @@ -0,0 +1,195 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +import java.io.BufferedReader; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Utility library for use in loading services using the Jar Service + * Provider Interface (Jar SPI). This can be replaced once the minimum + * java rev moves beyond Java 5. + */ +final class ServiceLib { + + /** + * Logger. + */ + private static final Logger LOG = + Logger.getLogger(ServiceLib.class.getName()); + + /////////////////////////////////////////////////////////////////////////// + // Package-private methods: + + /** + * Prevent construction. + */ + private ServiceLib() { + // Empty + } + + /////////////////////////////////////////////////////////////////////////// + // Package-private methods: + + /** + * Probe for and select an implementation of the specified service + * type by using the a modified Jar SPI mechanism. Modified in that + * the system properties will be checked to see if there is a value + * set for the naem of the class to be loaded. If so, that value is + * treated as the class name of the first implementation class to be + * attempted to be loaded. This provides a (unsupported) mechanism + * to insert other implementations. Note that the supported mechanism + * is by properly ordering the classpath. + * + * @return service instance + * @throws IllegalStateException is no service implementations could be + * instantiated + */ + static <T> T loadService(Class<T> ofType) { + List<String> implClasses = loadServicesImplementations(ofType); + for (String implClass : implClasses) { + T result = attemptLoad(ofType, implClass); + if (result != null) { + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest("Selected " + ofType.getSimpleName() + + " implementation: " + + result.getClass().getName()); + } + return result; + } + } + throw(new IllegalStateException( + "Could not load " + ofType.getName() + " implementation")); + } + + /////////////////////////////////////////////////////////////////////////// + // Private methods: + + /** + * Generates a list of implementation class names by using + * the Jar SPI technique. The order in which the class names occur + * in the service manifest is significant. + * + * @return list of all declared implementation class names + */ + private static List<String> loadServicesImplementations( + final Class ofClass) { + List<String> result = new ArrayList<String>(); + + // Allow a sysprop to specify the first candidate + String override = System.getProperty(ofClass.getName()); + if (override != null) { + result.add(override); + } + + ClassLoader loader = ServiceLib.class.getClassLoader(); + URL url = loader.getResource("META-INF/services/" + ofClass.getName()); + InputStream inStream = null; + InputStreamReader reader = null; + BufferedReader bReader = null; + try { + inStream = url.openStream(); + reader = new InputStreamReader(inStream); + bReader = new BufferedReader(reader); + String line; + while ((line = bReader.readLine()) != null) { + if (!line.matches("\\s*(#.*)?")) { + // not a comment or blank line + result.add(line.trim()); + } + } + } catch (IOException iox) { + LOG.log(Level.WARNING, + "Could not load services descriptor: " + url.toString(), + iox); + } finally { + finalClose(bReader); + finalClose(reader); + finalClose(inStream); + } + return result; + } + + /** + * Attempts to load the specified implementation class. + * Attempts will fail if - for example - the implementation depends + * on a class not found on the classpath. + * + * @param className implementation class to attempt to load + * @return service instance, or {@code null} if the instance could not be + * loaded + */ + private static <T> T attemptLoad( + final Class<T> ofClass, + final String className) { + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest("Attempting service load: " + className); + } + Level level; + Exception thrown; + try { + Class clazz = Class.forName(className); + if (!ofClass.isAssignableFrom(clazz)) { + if (LOG.isLoggable(Level.WARNING)) { + LOG.warning(clazz.getName() + " is not assignable to " + + ofClass.getName()); + } + return null; + } + return ofClass.cast(clazz.newInstance()); + } catch (ClassNotFoundException ex) { + level = Level.FINEST; + thrown = ex; + } catch (InstantiationException ex) { + level = Level.WARNING; + thrown = ex; + } catch (IllegalAccessException ex) { + level = Level.WARNING; + thrown = ex; + } + LOG.log(level, + "Could not load " + ofClass.getSimpleName() + + " instance: " + className, + thrown); + return null; + } + + /** + * Check and close a closeable object, trapping and ignoring any + * exception that might result. + * + * @param closeMe the thing to close + */ + private static void finalClose(final Closeable closeMe) { + if (closeMe != null) { + try { + closeMe.close(); + } catch (IOException iox) { + LOG.log(Level.FINEST, "Could not close: " + closeMe, iox); + } + } + } + +} diff --git a/src/com/kenai/jbosh/StaticBody.java b/src/com/kenai/jbosh/StaticBody.java new file mode 100644 index 0000000..fe225fb --- /dev/null +++ b/src/com/kenai/jbosh/StaticBody.java @@ -0,0 +1,133 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.Map; + +/** + * Implementation of the {@code AbstractBody} class which allows for the + * definition of messages from pre-existing message content. Instances of + * this class are based on the underlying data and therefore cannot be + * modified. In order to obtain the wrapper element namespace and + * attribute information, the body content is partially parsed. + * <p/> + * This class does only minimal syntactic and semantic checking with respect + * to what the generated XML will look like. It is up to the developer to + * protect against the definition of malformed XML messages when building + * instances of this class. + * <p/> + * Instances of this class are immutable and thread-safe. + */ +final class StaticBody extends AbstractBody { + + /** + * Selected parser to be used to process raw XML messages. + */ + private static final BodyParser PARSER = + new BodyParserXmlPull(); + + /** + * Size of the internal buffer when copying from a stream. + */ + private static final int BUFFER_SIZE = 1024; + + /** + * Map of all attributes to their values. + */ + private final Map<BodyQName, String> attrs; + + /** + * This body message in raw XML form. + */ + private final String raw; + + /////////////////////////////////////////////////////////////////////////// + // Constructors: + + /** + * Prevent direct construction. + */ + private StaticBody( + final Map<BodyQName, String> attrMap, + final String rawXML) { + attrs = attrMap; + raw = rawXML; + } + + /** + * Creates an instance which is initialized by reading a body + * message from the provided stream. + * + * @param inStream stream to read message XML from + * @return body instance + * @throws BOSHException on parse error + */ + public static StaticBody fromStream( + final InputStream inStream) + throws BOSHException { + ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); + try { + byte[] buffer = new byte[BUFFER_SIZE]; + int read; + do { + read = inStream.read(buffer); + if (read > 0) { + byteOut.write(buffer, 0, read); + } + } while (read >= 0); + } catch (IOException iox) { + throw(new BOSHException( + "Could not read body data", iox)); + } + return fromString(byteOut.toString()); + } + + /** + * Creates an instance which is initialized by reading a body + * message from the provided raw XML string. + * + * @param rawXML raw message XML in string form + * @return body instance + * @throws BOSHException on parse error + */ + public static StaticBody fromString( + final String rawXML) + throws BOSHException { + BodyParserResults results = PARSER.parse(rawXML); + return new StaticBody(results.getAttributes(), rawXML); + } + + + /** + * {@inheritDoc} + */ + public Map<BodyQName, String> getAttributes() { + return Collections.unmodifiableMap(attrs); + } + + /** + * {@inheritDoc} + */ + public String toXML() { + return raw; + } + +} diff --git a/src/com/kenai/jbosh/TerminalBindingCondition.java b/src/com/kenai/jbosh/TerminalBindingCondition.java new file mode 100644 index 0000000..0aecfd8 --- /dev/null +++ b/src/com/kenai/jbosh/TerminalBindingCondition.java @@ -0,0 +1,208 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +import java.util.HashMap; +import java.util.Map; + +/** + * Terminal binding conditions and their associated messages. + */ +final class TerminalBindingCondition { + + /** + * Map of condition names to condition instances. + */ + private static final Map<String, TerminalBindingCondition> + COND_TO_INSTANCE = new HashMap<String, TerminalBindingCondition>(); + + /** + * Map of HTTP response codes to condition instances. + */ + private static final Map<Integer, TerminalBindingCondition> + CODE_TO_INSTANCE = new HashMap<Integer, TerminalBindingCondition>(); + + static final TerminalBindingCondition BAD_REQUEST = + createWithCode("bad-request", "The format of an HTTP header or " + + "binding element received from the client is unacceptable " + + "(e.g., syntax error).", Integer.valueOf(400)); + + static final TerminalBindingCondition HOST_GONE = + create("host-gone", "The target domain specified in the 'to' " + + "attribute or the target host or port specified in the 'route' " + + "attribute is no longer serviced by the connection manager."); + + static final TerminalBindingCondition HOST_UNKNOWN = + create("host-unknown", "The target domain specified in the 'to' " + + "attribute or the target host or port specified in the 'route' " + + "attribute is unknown to the connection manager."); + + static final TerminalBindingCondition IMPROPER_ADDRESSING = + create("improper-addressing", "The initialization element lacks a " + + "'to' or 'route' attribute (or the attribute has no value) but " + + "the connection manager requires one."); + + static final TerminalBindingCondition INTERNAL_SERVER_ERROR = + create("internal-server-error", "The connection manager has " + + "experienced an internal error that prevents it from servicing " + + "the request."); + + static final TerminalBindingCondition ITEM_NOT_FOUND = + createWithCode("item-not-found", "(1) 'sid' is not valid, (2) " + + "'stream' is not valid, (3) 'rid' is larger than the upper limit " + + "of the expected window, (4) connection manager is unable to " + + "resend response, (5) 'key' sequence is invalid.", + Integer.valueOf(404)); + + static final TerminalBindingCondition OTHER_REQUEST = + create("other-request", "Another request being processed at the " + + "same time as this request caused the session to terminate."); + + static final TerminalBindingCondition POLICY_VIOLATION = + createWithCode("policy-violation", "The client has broken the " + + "session rules (polling too frequently, requesting too " + + "frequently, sending too many simultaneous requests).", + Integer.valueOf(403)); + + static final TerminalBindingCondition REMOTE_CONNECTION_FAILED = + create("remote-connection-failed", "The connection manager was " + + "unable to connect to, or unable to connect securely to, or has " + + "lost its connection to, the server."); + + static final TerminalBindingCondition REMOTE_STREAM_ERROR = + create("remote-stream-error", "Encapsulated transport protocol " + + "error."); + + static final TerminalBindingCondition SEE_OTHER_URI = + create("see-other-uri", "The connection manager does not operate " + + "at this URI (e.g., the connection manager accepts only SSL or " + + "TLS connections at some https: URI rather than the http: URI " + + "requested by the client)."); + + static final TerminalBindingCondition SYSTEM_SHUTDOWN = + create("system-shutdown", "The connection manager is being shut " + + "down. All active HTTP sessions are being terminated. No new " + + "sessions can be created."); + + static final TerminalBindingCondition UNDEFINED_CONDITION = + create("undefined-condition", "Unknown or undefined error " + + "condition."); + + /** + * Condition name. + */ + private final String cond; + + /** + * Descriptive message. + */ + private final String msg; + + /** + * Private constructor to pre + */ + private TerminalBindingCondition( + final String condition, + final String message) { + cond = condition; + msg = message; + } + + /** + * Helper method to call the helper method to add entries. + */ + private static TerminalBindingCondition create( + final String condition, + final String message) { + return createWithCode(condition, message, null); + } + + /** + * Helper method to add entries. + */ + private static TerminalBindingCondition createWithCode( + final String condition, + final String message, + final Integer code) { + if (condition == null) { + throw(new IllegalArgumentException( + "condition may not be null")); + } + if (message == null) { + throw(new IllegalArgumentException( + "message may not be null")); + } + if (COND_TO_INSTANCE.get(condition) != null) { + throw(new IllegalStateException( + "Multiple definitions of condition: " + condition)); + } + TerminalBindingCondition result = + new TerminalBindingCondition(condition, message); + COND_TO_INSTANCE.put(condition, result); + if (code != null) { + if (CODE_TO_INSTANCE.get(code) != null) { + throw(new IllegalStateException( + "Multiple definitions of code: " + code)); + } + CODE_TO_INSTANCE.put(code, result); + } + return result; + } + + /** + * Lookup the terminal binding condition instance with the condition + * name specified. + * + * @param condStr condition name + * @return terminal binding condition instance, or {@code null} if no + * instance is known by the name specified + */ + static TerminalBindingCondition forString(final String condStr) { + return COND_TO_INSTANCE.get(condStr); + } + + /** + * Lookup the terminal binding condition instance associated with the + * HTTP response code specified. + * + * @param httpRespCode HTTP response code + * @return terminal binding condition instance, or {@code null} if no + * instance is known by the response code specified + */ + static TerminalBindingCondition forHTTPResponseCode(final int httpRespCode) { + return CODE_TO_INSTANCE.get(Integer.valueOf(httpRespCode)); + } + + /** + * Get the name of the condition. + * + * @return condition name + */ + String getCondition() { + return cond; + } + + /** + * Get the human readable error message associated with this condition. + * + * @return error message + */ + String getMessage() { + return msg; + } + +} diff --git a/src/com/kenai/jbosh/ZLIBCodec.java b/src/com/kenai/jbosh/ZLIBCodec.java new file mode 100644 index 0000000..20844ad --- /dev/null +++ b/src/com/kenai/jbosh/ZLIBCodec.java @@ -0,0 +1,104 @@ +/* + * Copyright 2009 Mike Cumings + * + * 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. + */ + +package com.kenai.jbosh; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.InflaterInputStream; + +/** + * Codec methods for compressing and uncompressing using ZLIB. + */ +final class ZLIBCodec { + + /** + * Size of the internal buffer when decoding. + */ + private static final int BUFFER_SIZE = 512; + + /////////////////////////////////////////////////////////////////////////// + // Constructors: + + /** + * Prevent construction. + */ + private ZLIBCodec() { + // Empty + } + + /** + * Returns the name of the codec. + * + * @return string name of the codec (i.e., "deflate") + */ + public static String getID() { + return "deflate"; + } + + /** + * Compress/encode the data provided using the ZLIB format. + * + * @param data data to compress + * @return compressed data + * @throws IOException on compression failure + */ + public static byte[] encode(final byte[] data) throws IOException { + ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); + DeflaterOutputStream deflateOut = null; + try { + deflateOut = new DeflaterOutputStream(byteOut); + deflateOut.write(data); + deflateOut.close(); + byteOut.close(); + return byteOut.toByteArray(); + } finally { + deflateOut.close(); + byteOut.close(); + } + } + + /** + * Uncompress/decode the data provided using the ZLIB format. + * + * @param data data to uncompress + * @return uncompressed data + * @throws IOException on decompression failure + */ + public static byte[] decode(final byte[] compressed) throws IOException { + ByteArrayInputStream byteIn = new ByteArrayInputStream(compressed); + ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); + InflaterInputStream inflaterIn = null; + try { + inflaterIn = new InflaterInputStream(byteIn); + int read; + byte[] buffer = new byte[BUFFER_SIZE]; + do { + read = inflaterIn.read(buffer); + if (read > 0) { + byteOut.write(buffer, 0, read); + } + } while (read >= 0); + return byteOut.toByteArray(); + } finally { + inflaterIn.close(); + byteOut.close(); + } + } + +} diff --git a/src/com/kenai/jbosh/package.html b/src/com/kenai/jbosh/package.html new file mode 100644 index 0000000..77a1924 --- /dev/null +++ b/src/com/kenai/jbosh/package.html @@ -0,0 +1,8 @@ +<html> + <body> + Core classes of the JBOSH API. + <p/> + Users of the client portion of the API should start by reading + up on the <code>BOSHClient</code> documentation. + </body> +</html>
\ No newline at end of file diff --git a/src/com/novell/sasl/client/DigestChallenge.java b/src/com/novell/sasl/client/DigestChallenge.java new file mode 100644 index 0000000..90e6247 --- /dev/null +++ b/src/com/novell/sasl/client/DigestChallenge.java @@ -0,0 +1,393 @@ +/* ************************************************************************** + * $OpenLDAP: /com/novell/sasl/client/DigestChallenge.java,v 1.3 2005/01/17 15:00:54 sunilk Exp $ + * + * Copyright (C) 2003 Novell, Inc. All Rights Reserved. + * + * THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND + * TREATIES. USE, MODIFICATION, AND REDISTRIBUTION OF THIS WORK IS SUBJECT + * TO VERSION 2.0.1 OF THE OPENLDAP PUBLIC LICENSE, A COPY OF WHICH IS + * AVAILABLE AT HTTP://WWW.OPENLDAP.ORG/LICENSE.HTML OR IN THE FILE "LICENSE" + * IN THE TOP-LEVEL DIRECTORY OF THE DISTRIBUTION. ANY USE OR EXPLOITATION + * OF THIS WORK OTHER THAN AS AUTHORIZED IN VERSION 2.0.1 OF THE OPENLDAP + * PUBLIC LICENSE, OR OTHER PRIOR WRITTEN CONSENT FROM NOVELL, COULD SUBJECT + * THE PERPETRATOR TO CRIMINAL AND CIVIL LIABILITY. + ******************************************************************************/ +package com.novell.sasl.client; + +import java.util.*; +import org.apache.harmony.javax.security.sasl.*; + +/** + * Implements the DigestChallenge class which will be used by the + * DigestMD5SaslClient class + */ +class DigestChallenge extends Object +{ + public static final int QOP_AUTH = 0x01; + public static final int QOP_AUTH_INT = 0x02; + public static final int QOP_AUTH_CONF = 0x04; + public static final int QOP_UNRECOGNIZED = 0x08; + + private static final int CIPHER_3DES = 0x01; + private static final int CIPHER_DES = 0x02; + private static final int CIPHER_RC4_40 = 0x04; + private static final int CIPHER_RC4 = 0x08; + private static final int CIPHER_RC4_56 = 0x10; + private static final int CIPHER_UNRECOGNIZED = 0x20; + private static final int CIPHER_RECOGNIZED_MASK = + CIPHER_3DES | CIPHER_DES | CIPHER_RC4_40 | CIPHER_RC4 | CIPHER_RC4_56; + + private ArrayList m_realms; + private String m_nonce; + private int m_qop; + private boolean m_staleFlag; + private int m_maxBuf; + private String m_characterSet; + private String m_algorithm; + private int m_cipherOptions; + + DigestChallenge( + byte[] challenge) + throws SaslException + { + m_realms = new ArrayList(5); + m_nonce = null; + m_qop = 0; + m_staleFlag = false; + m_maxBuf = -1; + m_characterSet = null; + m_algorithm = null; + m_cipherOptions = 0; + + DirectiveList dirList = new DirectiveList(challenge); + try + { + dirList.parseDirectives(); + checkSemantics(dirList); + } + catch (SaslException e) + { + } + } + + /** + * Checks the semantics of the directives in the directive list as parsed + * from the digest challenge byte array. + * + * @param dirList the list of directives parsed from the digest challenge + * + * @exception SaslException If a semantic error occurs + */ + void checkSemantics( + DirectiveList dirList) throws SaslException + { + Iterator directives = dirList.getIterator(); + ParsedDirective directive; + String name; + + while (directives.hasNext()) + { + directive = (ParsedDirective)directives.next(); + name = directive.getName(); + if (name.equals("realm")) + handleRealm(directive); + else if (name.equals("nonce")) + handleNonce(directive); + else if (name.equals("qop")) + handleQop(directive); + else if (name.equals("maxbuf")) + handleMaxbuf(directive); + else if (name.equals("charset")) + handleCharset(directive); + else if (name.equals("algorithm")) + handleAlgorithm(directive); + else if (name.equals("cipher")) + handleCipher(directive); + else if (name.equals("stale")) + handleStale(directive); + } + + /* post semantic check */ + if (-1 == m_maxBuf) + m_maxBuf = 65536; + + if (m_qop == 0) + m_qop = QOP_AUTH; + else if ( (m_qop & QOP_AUTH) != QOP_AUTH ) + throw new SaslException("Only qop-auth is supported by client"); + else if ( ((m_qop & QOP_AUTH_CONF) == QOP_AUTH_CONF) && + (0 == (m_cipherOptions & CIPHER_RECOGNIZED_MASK)) ) + throw new SaslException("Invalid cipher options"); + else if (null == m_nonce) + throw new SaslException("Missing nonce directive"); + else if (m_staleFlag) + throw new SaslException("Unexpected stale flag"); + else if ( null == m_algorithm ) + throw new SaslException("Missing algorithm directive"); + } + + /** + * This function implements the semenatics of the nonce directive. + * + * @param pd ParsedDirective + * + * @exception SaslException If an error occurs due to too many nonce + * values + */ + void handleNonce( + ParsedDirective pd) throws SaslException + { + if (null != m_nonce) + throw new SaslException("Too many nonce values."); + + m_nonce = pd.getValue(); + } + + /** + * This function implements the semenatics of the realm directive. + * + * @param pd ParsedDirective + */ + void handleRealm( + ParsedDirective pd) + { + m_realms.add(pd.getValue()); + } + + /** + * This function implements the semenatics of the qop (quality of protection) + * directive. The value of the qop directive is as defined below: + * qop-options = "qop" "=" <"> qop-list <"> + * qop-list = 1#qop-value + * qop-value = "auth" | "auth-int" | "auth-conf" | token + * + * @param pd ParsedDirective + * + * @exception SaslException If an error occurs due to too many qop + * directives + */ + void handleQop( + ParsedDirective pd) throws SaslException + { + String token; + TokenParser parser; + + if (m_qop != 0) + throw new SaslException("Too many qop directives."); + + parser = new TokenParser(pd.getValue()); + for (token = parser.parseToken(); + token != null; + token = parser.parseToken()) + { + if (token.equals("auth")) + m_qop |= QOP_AUTH; + else if (token.equals("auth-int")) + m_qop |= QOP_AUTH_INT; + else if (token.equals("auth-conf")) + m_qop |= QOP_AUTH_CONF; + else + m_qop |= QOP_UNRECOGNIZED; + } + } + + /** + * This function implements the semenatics of the Maxbuf directive. + * the value is defined as: 1*DIGIT + * + * @param pd ParsedDirective + * + * @exception SaslException If an error occur + */ + void handleMaxbuf( + ParsedDirective pd) throws SaslException + { + if (-1 != m_maxBuf) /*it's initialized to -1 */ + throw new SaslException("Too many maxBuf directives."); + + m_maxBuf = Integer.parseInt(pd.getValue()); + + if (0 == m_maxBuf) + throw new SaslException("Max buf value must be greater than zero."); + } + + /** + * This function implements the semenatics of the charset directive. + * the value is defined as: 1*DIGIT + * + * @param pd ParsedDirective + * + * @exception SaslException If an error occurs dur to too many charset + * directives or Invalid character encoding + * directive + */ + void handleCharset( + ParsedDirective pd) throws SaslException + { + if (null != m_characterSet) + throw new SaslException("Too many charset directives."); + + m_characterSet = pd.getValue(); + + if (!m_characterSet.equals("utf-8")) + throw new SaslException("Invalid character encoding directive"); + } + + /** + * This function implements the semenatics of the charset directive. + * the value is defined as: 1*DIGIT + * + * @param pd ParsedDirective + * + * @exception SaslException If an error occurs due to too many algorith + * directive or Invalid algorithm directive + * value + */ + void handleAlgorithm( + ParsedDirective pd) throws SaslException + { + if (null != m_algorithm) + throw new SaslException("Too many algorithm directives."); + + m_algorithm = pd.getValue(); + + if (!"md5-sess".equals(m_algorithm)) + throw new SaslException("Invalid algorithm directive value: " + + m_algorithm); + } + + /** + * This function implements the semenatics of the cipher-opts directive + * directive. The value of the qop directive is as defined below: + * qop-options = "qop" "=" <"> qop-list <"> + * qop-list = 1#qop-value + * qop-value = "auth" | "auth-int" | "auth-conf" | token + * + * @param pd ParsedDirective + * + * @exception SaslException If an error occurs due to Too many cipher + * directives + */ + void handleCipher( + ParsedDirective pd) throws SaslException + { + String token; + TokenParser parser; + + if (0 != m_cipherOptions) + throw new SaslException("Too many cipher directives."); + + parser = new TokenParser(pd.getValue()); + token = parser.parseToken(); + for (token = parser.parseToken(); + token != null; + token = parser.parseToken()) + { + if ("3des".equals(token)) + m_cipherOptions |= CIPHER_3DES; + else if ("des".equals(token)) + m_cipherOptions |= CIPHER_DES; + else if ("rc4-40".equals(token)) + m_cipherOptions |= CIPHER_RC4_40; + else if ("rc4".equals(token)) + m_cipherOptions |= CIPHER_RC4; + else if ("rc4-56".equals(token)) + m_cipherOptions |= CIPHER_RC4_56; + else + m_cipherOptions |= CIPHER_UNRECOGNIZED; + } + + if (m_cipherOptions == 0) + m_cipherOptions = CIPHER_UNRECOGNIZED; + } + + /** + * This function implements the semenatics of the stale directive. + * + * @param pd ParsedDirective + * + * @exception SaslException If an error occurs due to Too many stale + * directives or Invalid stale directive value + */ + void handleStale( + ParsedDirective pd) throws SaslException + { + if (false != m_staleFlag) + throw new SaslException("Too many stale directives."); + + if ("true".equals(pd.getValue())) + m_staleFlag = true; + else + throw new SaslException("Invalid stale directive value: " + + pd.getValue()); + } + + /** + * Return the list of the All the Realms + * + * @return List of all the realms + */ + public ArrayList getRealms() + { + return m_realms; + } + + /** + * @return Returns the Nonce + */ + public String getNonce() + { + return m_nonce; + } + + /** + * Return the quality-of-protection + * + * @return The quality-of-protection + */ + public int getQop() + { + return m_qop; + } + + /** + * @return The state of the Staleflag + */ + public boolean getStaleFlag() + { + return m_staleFlag; + } + + /** + * @return The Maximum Buffer value + */ + public int getMaxBuf() + { + return m_maxBuf; + } + + /** + * @return character set values as string + */ + public String getCharacterSet() + { + return m_characterSet; + } + + /** + * @return The String value of the algorithm + */ + public String getAlgorithm() + { + return m_algorithm; + } + + /** + * @return The cipher options + */ + public int getCipherOptions() + { + return m_cipherOptions; + } +} + diff --git a/src/com/novell/sasl/client/DigestMD5SaslClient.java b/src/com/novell/sasl/client/DigestMD5SaslClient.java new file mode 100644 index 0000000..141c96b --- /dev/null +++ b/src/com/novell/sasl/client/DigestMD5SaslClient.java @@ -0,0 +1,820 @@ +/* ************************************************************************** + * $OpenLDAP: /com/novell/sasl/client/DigestMD5SaslClient.java,v 1.4 2005/01/17 15:00:54 sunilk Exp $ + * + * Copyright (C) 2003 Novell, Inc. All Rights Reserved. + * + * THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND + * TREATIES. USE, MODIFICATION, AND REDISTRIBUTION OF THIS WORK IS SUBJECT + * TO VERSION 2.0.1 OF THE OPENLDAP PUBLIC LICENSE, A COPY OF WHICH IS + * AVAILABLE AT HTTP://WWW.OPENLDAP.ORG/LICENSE.HTML OR IN THE FILE "LICENSE" + * IN THE TOP-LEVEL DIRECTORY OF THE DISTRIBUTION. ANY USE OR EXPLOITATION + * OF THIS WORK OTHER THAN AS AUTHORIZED IN VERSION 2.0.1 OF THE OPENLDAP + * PUBLIC LICENSE, OR OTHER PRIOR WRITTEN CONSENT FROM NOVELL, COULD SUBJECT + * THE PERPETRATOR TO CRIMINAL AND CIVIL LIABILITY. + ******************************************************************************/ +package com.novell.sasl.client; + +import org.apache.harmony.javax.security.sasl.*; +import org.apache.harmony.javax.security.auth.callback.*; +import java.security.SecureRandom; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.io.UnsupportedEncodingException; +import java.io.IOException; +import java.util.*; + +/** + * Implements the Client portion of DigestMD5 Sasl mechanism. + */ +public class DigestMD5SaslClient implements SaslClient +{ + private String m_authorizationId = ""; + private String m_protocol = ""; + private String m_serverName = ""; + private Map m_props; + private CallbackHandler m_cbh; + private int m_state; + private String m_qopValue = ""; + private char[] m_HA1 = null; + private String m_digestURI; + private DigestChallenge m_dc; + private String m_clientNonce = ""; + private String m_realm = ""; + private String m_name = ""; + + private static final int STATE_INITIAL = 0; + private static final int STATE_DIGEST_RESPONSE_SENT = 1; + private static final int STATE_VALID_SERVER_RESPONSE = 2; + private static final int STATE_INVALID_SERVER_RESPONSE = 3; + private static final int STATE_DISPOSED = 4; + + private static final int NONCE_BYTE_COUNT = 32; + private static final int NONCE_HEX_COUNT = 2*NONCE_BYTE_COUNT; + + private static final String DIGEST_METHOD = "AUTHENTICATE"; + + /** + * Creates an DigestMD5SaslClient object using the parameters supplied. + * Assumes that the QOP, STRENGTH, and SERVER_AUTH properties are + * contained in props + * + * @param authorizationId The possibly null protocol-dependent + * identification to be used for authorization. If + * null or empty, the server derives an authorization + * ID from the client's authentication credentials. + * When the SASL authentication completes + * successfully, the specified entity is granted + * access. + * + * @param protocol The non-null string name of the protocol for which + * the authentication is being performed (e.g. "ldap") + * + * @param serverName The non-null fully qualified host name of the server + * to authenticate to + * + * @param props The possibly null set of properties used to select + * the SASL mechanism and to configure the + * authentication exchange of the selected mechanism. + * See the Sasl class for a list of standard properties. + * Other, possibly mechanism-specific, properties can + * be included. Properties not relevant to the selected + * mechanism are ignored. + * + * @param cbh The possibly null callback handler to used by the + * SASL mechanisms to get further information from the + * application/library to complete the authentication. + * For example, a SASL mechanism might require the + * authentication ID, password and realm from the + * caller. The authentication ID is requested by using + * a NameCallback. The password is requested by using + * a PasswordCallback. The realm is requested by using + * a RealmChoiceCallback if there is a list of realms + * to choose from, and by using a RealmCallback if the + * realm must be entered. + * + * @return A possibly null SaslClient created using the + * parameters supplied. If null, this factory cannot + * produce a SaslClient using the parameters supplied. + * + * @exception SaslException If a SaslClient instance cannot be created + * because of an error + */ + public static SaslClient getClient( + String authorizationId, + String protocol, + String serverName, + Map props, + CallbackHandler cbh) + { + String desiredQOP = (String)props.get(Sasl.QOP); + String desiredStrength = (String)props.get(Sasl.STRENGTH); + String serverAuth = (String)props.get(Sasl.SERVER_AUTH); + + //only support qop equal to auth + if ((desiredQOP != null) && !"auth".equals(desiredQOP)) + return null; + + //doesn't support server authentication + if ((serverAuth != null) && !"false".equals(serverAuth)) + return null; + + //need a callback handler to get the password + if (cbh == null) + return null; + + return new DigestMD5SaslClient(authorizationId, protocol, + serverName, props, cbh); + } + + /** + * Creates an DigestMD5SaslClient object using the parameters supplied. + * Assumes that the QOP, STRENGTH, and SERVER_AUTH properties are + * contained in props + * + * @param authorizationId The possibly null protocol-dependent + * identification to be used for authorization. If + * null or empty, the server derives an authorization + * ID from the client's authentication credentials. + * When the SASL authentication completes + * successfully, the specified entity is granted + * access. + * + * @param protocol The non-null string name of the protocol for which + * the authentication is being performed (e.g. "ldap") + * + * @param serverName The non-null fully qualified host name of the server + * to authenticate to + * + * @param props The possibly null set of properties used to select + * the SASL mechanism and to configure the + * authentication exchange of the selected mechanism. + * See the Sasl class for a list of standard properties. + * Other, possibly mechanism-specific, properties can + * be included. Properties not relevant to the selected + * mechanism are ignored. + * + * @param cbh The possibly null callback handler to used by the + * SASL mechanisms to get further information from the + * application/library to complete the authentication. + * For example, a SASL mechanism might require the + * authentication ID, password and realm from the + * caller. The authentication ID is requested by using + * a NameCallback. The password is requested by using + * a PasswordCallback. The realm is requested by using + * a RealmChoiceCallback if there is a list of realms + * to choose from, and by using a RealmCallback if the + * realm must be entered. + * + */ + private DigestMD5SaslClient( + String authorizationId, + String protocol, + String serverName, + Map props, + CallbackHandler cbh) + { + m_authorizationId = authorizationId; + m_protocol = protocol; + m_serverName = serverName; + m_props = props; + m_cbh = cbh; + + m_state = STATE_INITIAL; + } + + /** + * Determines if this mechanism has an optional initial response. If true, + * caller should call evaluateChallenge() with an empty array to get the + * initial response. + * + * @return true if this mechanism has an initial response + */ + public boolean hasInitialResponse() + { + return false; + } + + /** + * Determines if the authentication exchange has completed. This method + * may be called at any time, but typically, it will not be called until + * the caller has received indication from the server (in a protocol- + * specific manner) that the exchange has completed. + * + * @return true if the authentication exchange has completed; + * false otherwise. + */ + public boolean isComplete() + { + if ((m_state == STATE_VALID_SERVER_RESPONSE) || + (m_state == STATE_INVALID_SERVER_RESPONSE) || + (m_state == STATE_DISPOSED)) + return true; + else + return false; + } + + /** + * Unwraps a byte array received from the server. This method can be called + * only after the authentication exchange has completed (i.e., when + * isComplete() returns true) and only if the authentication exchange has + * negotiated integrity and/or privacy as the quality of protection; + * otherwise, an IllegalStateException is thrown. + * + * incoming is the contents of the SASL buffer as defined in RFC 2222 + * without the leading four octet field that represents the length. + * offset and len specify the portion of incoming to use. + * + * @param incoming A non-null byte array containing the encoded bytes + * from the server + * @param offset The starting position at incoming of the bytes to use + * + * @param len The number of bytes from incoming to use + * + * @return A non-null byte array containing the decoded bytes + * + */ + public byte[] unwrap( + byte[] incoming, + int offset, + int len) + throws SaslException + { + throw new IllegalStateException( + "unwrap: QOP has neither integrity nor privacy>"); + } + + /** + * Wraps a byte array to be sent to the server. This method can be called + * only after the authentication exchange has completed (i.e., when + * isComplete() returns true) and only if the authentication exchange has + * negotiated integrity and/or privacy as the quality of protection; + * otherwise, an IllegalStateException is thrown. + * + * The result of this method will make up the contents of the SASL buffer as + * defined in RFC 2222 without the leading four octet field that represents + * the length. offset and len specify the portion of outgoing to use. + * + * @param outgoing A non-null byte array containing the bytes to encode + * @param offset The starting position at outgoing of the bytes to use + * @param len The number of bytes from outgoing to use + * + * @return A non-null byte array containing the encoded bytes + * + * @exception SaslException if incoming cannot be successfully unwrapped. + * + * @exception IllegalStateException if the authentication exchange has + * not completed, or if the negotiated quality of + * protection has neither integrity nor privacy. + */ + public byte[] wrap( + byte[] outgoing, + int offset, + int len) + throws SaslException + { + throw new IllegalStateException( + "wrap: QOP has neither integrity nor privacy>"); + } + + /** + * Retrieves the negotiated property. This method can be called only after + * the authentication exchange has completed (i.e., when isComplete() + * returns true); otherwise, an IllegalStateException is thrown. + * + * @param propName The non-null property name + * + * @return The value of the negotiated property. If null, the property was + * not negotiated or is not applicable to this mechanism. + * + * @exception IllegalStateException if this authentication exchange has + * not completed + */ + public Object getNegotiatedProperty( + String propName) + { + if (m_state != STATE_VALID_SERVER_RESPONSE) + throw new IllegalStateException( + "getNegotiatedProperty: authentication exchange not complete."); + + if (Sasl.QOP.equals(propName)) + return "auth"; + else + return null; + } + + /** + * Disposes of any system resources or security-sensitive information the + * SaslClient might be using. Invoking this method invalidates the + * SaslClient instance. This method is idempotent. + * + * @exception SaslException if a problem was encountered while disposing + * of the resources + */ + public void dispose() + throws SaslException + { + if (m_state != STATE_DISPOSED) + { + m_state = STATE_DISPOSED; + } + } + + /** + * Evaluates the challenge data and generates a response. If a challenge + * is received from the server during the authentication process, this + * method is called to prepare an appropriate next response to submit to + * the server. + * + * @param challenge The non-null challenge sent from the server. The + * challenge array may have zero length. + * + * @return The possibly null reponse to send to the server. It is null + * if the challenge accompanied a "SUCCESS" status and the + * challenge only contains data for the client to update its + * state and no response needs to be sent to the server. + * The response is a zero-length byte array if the client is to + * send a response with no data. + * + * @exception SaslException If an error occurred while processing the + * challenge or generating a response. + */ + public byte[] evaluateChallenge( + byte[] challenge) + throws SaslException + { + byte[] response = null; + + //printState(); + switch (m_state) + { + case STATE_INITIAL: + if (challenge.length == 0) + throw new SaslException("response = byte[0]"); + else + try + { + response = createDigestResponse(challenge). + getBytes("UTF-8"); + m_state = STATE_DIGEST_RESPONSE_SENT; + } + catch (java.io.UnsupportedEncodingException e) + { + throw new SaslException( + "UTF-8 encoding not suppported by platform", e); + } + break; + case STATE_DIGEST_RESPONSE_SENT: + if (checkServerResponseAuth(challenge)) + m_state = STATE_VALID_SERVER_RESPONSE; + else + { + m_state = STATE_INVALID_SERVER_RESPONSE; + throw new SaslException("Could not validate response-auth " + + "value from server"); + } + break; + case STATE_VALID_SERVER_RESPONSE: + case STATE_INVALID_SERVER_RESPONSE: + throw new SaslException("Authentication sequence is complete"); + case STATE_DISPOSED: + throw new SaslException("Client has been disposed"); + default: + throw new SaslException("Unknown client state."); + } + + return response; + } + + /** + * This function takes a 16 byte binary md5-hash value and creates a 32 + * character (plus a terminating null character) hex-digit + * representation of binary data. + * + * @param hash 16 byte binary md5-hash value in bytes + * + * @return 32 character (plus a terminating null character) hex-digit + * representation of binary data. + */ + char[] convertToHex( + byte[] hash) + { + int i; + byte j; + byte fifteen = 15; + char[] hex = new char[32]; + + for (i = 0; i < 16; i++) + { + //convert value of top 4 bits to hex char + hex[i*2] = getHexChar((byte)((hash[i] & 0xf0) >> 4)); + //convert value of bottom 4 bits to hex char + hex[(i*2)+1] = getHexChar((byte)(hash[i] & 0x0f)); + } + + return hex; + } + + /** + * Calculates the HA1 portion of the response + * + * @param algorithm Algorith to use. + * @param userName User being authenticated + * @param realm realm information + * @param password password of teh user + * @param nonce nonce value + * @param clientNonce Clients Nonce value + * + * @return HA1 portion of the response in a character array + * + * @exception SaslException If an error occurs + */ + char[] DigestCalcHA1( + String algorithm, + String userName, + String realm, + String password, + String nonce, + String clientNonce) throws SaslException + { + byte[] hash; + + try + { + MessageDigest md = MessageDigest.getInstance("MD5"); + + md.update(userName.getBytes("UTF-8")); + md.update(":".getBytes("UTF-8")); + md.update(realm.getBytes("UTF-8")); + md.update(":".getBytes("UTF-8")); + md.update(password.getBytes("UTF-8")); + hash = md.digest(); + + if ("md5-sess".equals(algorithm)) + { + md.update(hash); + md.update(":".getBytes("UTF-8")); + md.update(nonce.getBytes("UTF-8")); + md.update(":".getBytes("UTF-8")); + md.update(clientNonce.getBytes("UTF-8")); + hash = md.digest(); + } + } + catch(NoSuchAlgorithmException e) + { + throw new SaslException("No provider found for MD5 hash", e); + } + catch(UnsupportedEncodingException e) + { + throw new SaslException( + "UTF-8 encoding not supported by platform.", e); + } + + return convertToHex(hash); + } + + + /** + * This function calculates the response-value of the response directive of + * the digest-response as documented in RFC 2831 + * + * @param HA1 H(A1) + * @param serverNonce nonce from server + * @param nonceCount 8 hex digits + * @param clientNonce client nonce + * @param qop qop-value: "", "auth", "auth-int" + * @param method method from the request + * @param digestUri requested URL + * @param clientResponseFlag request-digest or response-digest + * + * @return Response-value of the response directive of the digest-response + * + * @exception SaslException If an error occurs + */ + char[] DigestCalcResponse( + char[] HA1, /* H(A1) */ + String serverNonce, /* nonce from server */ + String nonceCount, /* 8 hex digits */ + String clientNonce, /* client nonce */ + String qop, /* qop-value: "", "auth", "auth-int" */ + String method, /* method from the request */ + String digestUri, /* requested URL */ + boolean clientResponseFlag) /* request-digest or response-digest */ + throws SaslException + { + byte[] HA2; + byte[] respHash; + char[] HA2Hex; + + // calculate H(A2) + try + { + MessageDigest md = MessageDigest.getInstance("MD5"); + if (clientResponseFlag) + md.update(method.getBytes("UTF-8")); + md.update(":".getBytes("UTF-8")); + md.update(digestUri.getBytes("UTF-8")); + if ("auth-int".equals(qop)) + { + md.update(":".getBytes("UTF-8")); + md.update("00000000000000000000000000000000".getBytes("UTF-8")); + } + HA2 = md.digest(); + HA2Hex = convertToHex(HA2); + + // calculate response + md.update(new String(HA1).getBytes("UTF-8")); + md.update(":".getBytes("UTF-8")); + md.update(serverNonce.getBytes("UTF-8")); + md.update(":".getBytes("UTF-8")); + if (qop.length() > 0) + { + md.update(nonceCount.getBytes("UTF-8")); + md.update(":".getBytes("UTF-8")); + md.update(clientNonce.getBytes("UTF-8")); + md.update(":".getBytes("UTF-8")); + md.update(qop.getBytes("UTF-8")); + md.update(":".getBytes("UTF-8")); + } + md.update(new String(HA2Hex).getBytes("UTF-8")); + respHash = md.digest(); + } + catch(NoSuchAlgorithmException e) + { + throw new SaslException("No provider found for MD5 hash", e); + } + catch(UnsupportedEncodingException e) + { + throw new SaslException( + "UTF-8 encoding not supported by platform.", e); + } + + return convertToHex(respHash); + } + + + /** + * Creates the intial response to be sent to the server. + * + * @param challenge Challenge in bytes recived form the Server + * + * @return Initial response to be sent to the server + */ + private String createDigestResponse( + byte[] challenge) + throws SaslException + { + char[] response; + StringBuffer digestResponse = new StringBuffer(512); + int realmSize; + + m_dc = new DigestChallenge(challenge); + + m_digestURI = m_protocol + "/" + m_serverName; + + if ((m_dc.getQop() & DigestChallenge.QOP_AUTH) + == DigestChallenge.QOP_AUTH ) + m_qopValue = "auth"; + else + throw new SaslException("Client only supports qop of 'auth'"); + + //get call back information + Callback[] callbacks = new Callback[3]; + ArrayList realms = m_dc.getRealms(); + realmSize = realms.size(); + if (realmSize == 0) + { + callbacks[0] = new RealmCallback("Realm"); + } + else if (realmSize == 1) + { + callbacks[0] = new RealmCallback("Realm", (String)realms.get(0)); + } + else + { + callbacks[0] = + new RealmChoiceCallback( + "Realm", + (String[])realms.toArray(new String[realmSize]), + 0, //the default choice index + false); //no multiple selections + } + + callbacks[1] = new PasswordCallback("Password", false); + //false = no echo + + if (m_authorizationId == null || m_authorizationId.length() == 0) + callbacks[2] = new NameCallback("Name"); + else + callbacks[2] = new NameCallback("Name", m_authorizationId); + + try + { + m_cbh.handle(callbacks); + } + catch(UnsupportedCallbackException e) + { + throw new SaslException("Handler does not support" + + " necessary callbacks",e); + } + catch(IOException e) + { + throw new SaslException("IO exception in CallbackHandler.", e); + } + + if (realmSize > 1) + { + int[] selections = + ((RealmChoiceCallback)callbacks[0]).getSelectedIndexes(); + + if (selections.length > 0) + m_realm = + ((RealmChoiceCallback)callbacks[0]).getChoices()[selections[0]]; + else + m_realm = ((RealmChoiceCallback)callbacks[0]).getChoices()[0]; + } + else + m_realm = ((RealmCallback)callbacks[0]).getText(); + + m_clientNonce = getClientNonce(); + + m_name = ((NameCallback)callbacks[2]).getName(); + if (m_name == null) + m_name = ((NameCallback)callbacks[2]).getDefaultName(); + if (m_name == null) + throw new SaslException("No user name was specified."); + + m_HA1 = DigestCalcHA1( + m_dc.getAlgorithm(), + m_name, + m_realm, + new String(((PasswordCallback)callbacks[1]).getPassword()), + m_dc.getNonce(), + m_clientNonce); + + response = DigestCalcResponse(m_HA1, + m_dc.getNonce(), + "00000001", + m_clientNonce, + m_qopValue, + "AUTHENTICATE", + m_digestURI, + true); + + digestResponse.append("username=\""); + digestResponse.append(m_authorizationId); + if (0 != m_realm.length()) + { + digestResponse.append("\",realm=\""); + digestResponse.append(m_realm); + } + digestResponse.append("\",cnonce=\""); + digestResponse.append(m_clientNonce); + digestResponse.append("\",nc="); + digestResponse.append("00000001"); //nounce count + digestResponse.append(",qop="); + digestResponse.append(m_qopValue); + digestResponse.append(",digest-uri=\""); + digestResponse.append(m_digestURI); + digestResponse.append("\",response="); + digestResponse.append(response); + digestResponse.append(",charset=utf-8,nonce=\""); + digestResponse.append(m_dc.getNonce()); + digestResponse.append("\""); + + return digestResponse.toString(); + } + + + /** + * This function validates the server response. This step performs a + * modicum of mutual authentication by verifying that the server knows + * the user's password + * + * @param serverResponse Response recived form Server + * + * @return true if the mutual authentication succeeds; + * else return false + * + * @exception SaslException If an error occurs + */ + boolean checkServerResponseAuth( + byte[] serverResponse) throws SaslException + { + char[] response; + ResponseAuth responseAuth = null; + String responseStr; + + responseAuth = new ResponseAuth(serverResponse); + + response = DigestCalcResponse(m_HA1, + m_dc.getNonce(), + "00000001", + m_clientNonce, + m_qopValue, + DIGEST_METHOD, + m_digestURI, + false); + + responseStr = new String(response); + + return responseStr.equals(responseAuth.getResponseValue()); + } + + + /** + * This function returns hex character representing the value of the input + * + * @param value Input value in byte + * + * @return Hex value of the Input byte value + */ + private static char getHexChar( + byte value) + { + switch (value) + { + case 0: + return '0'; + case 1: + return '1'; + case 2: + return '2'; + case 3: + return '3'; + case 4: + return '4'; + case 5: + return '5'; + case 6: + return '6'; + case 7: + return '7'; + case 8: + return '8'; + case 9: + return '9'; + case 10: + return 'a'; + case 11: + return 'b'; + case 12: + return 'c'; + case 13: + return 'd'; + case 14: + return 'e'; + case 15: + return 'f'; + default: + return 'Z'; + } + } + + /** + * Calculates the Nonce value of the Client + * + * @return Nonce value of the client + * + * @exception SaslException If an error Occurs + */ + String getClientNonce() throws SaslException + { + byte[] nonceBytes = new byte[NONCE_BYTE_COUNT]; + SecureRandom prng; + byte nonceByte; + char[] hexNonce = new char[NONCE_HEX_COUNT]; + + try + { + prng = SecureRandom.getInstance("SHA1PRNG"); + prng.nextBytes(nonceBytes); + for(int i=0; i<NONCE_BYTE_COUNT; i++) + { + //low nibble + hexNonce[i*2] = getHexChar((byte)(nonceBytes[i] & 0x0f)); + //high nibble + hexNonce[(i*2)+1] = getHexChar((byte)((nonceBytes[i] & 0xf0) + >> 4)); + } + return new String(hexNonce); + } + catch(NoSuchAlgorithmException e) + { + throw new SaslException("No random number generator available", e); + } + } + + /** + * Returns the IANA-registered mechanism name of this SASL client. + * (e.g. "CRAM-MD5", "GSSAPI") + * + * @return "DIGEST-MD5"the IANA-registered mechanism name of this SASL + * client. + */ + public String getMechanismName() + { + return "DIGEST-MD5"; + } + +} //end class DigestMD5SaslClient + diff --git a/src/com/novell/sasl/client/DirectiveList.java b/src/com/novell/sasl/client/DirectiveList.java new file mode 100644 index 0000000..fc26a6b --- /dev/null +++ b/src/com/novell/sasl/client/DirectiveList.java @@ -0,0 +1,363 @@ +/* ************************************************************************** + * $OpenLDAP: /com/novell/sasl/client/DirectiveList.java,v 1.4 2005/01/17 15:00:54 sunilk Exp $ + * + * Copyright (C) 2002 Novell, Inc. All Rights Reserved. + * + * THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND + * TREATIES. USE, MODIFICATION, AND REDISTRIBUTION OF THIS WORK IS SUBJECT + * TO VERSION 2.0.1 OF THE OPENLDAP PUBLIC LICENSE, A COPY OF WHICH IS + * AVAILABLE AT HTTP://WWW.OPENLDAP.ORG/LICENSE.HTML OR IN THE FILE "LICENSE" + * IN THE TOP-LEVEL DIRECTORY OF THE DISTRIBUTION. ANY USE OR EXPLOITATION + * OF THIS WORK OTHER THAN AS AUTHORIZED IN VERSION 2.0.1 OF THE OPENLDAP + * PUBLIC LICENSE, OR OTHER PRIOR WRITTEN CONSENT FROM NOVELL, COULD SUBJECT + * THE PERPETRATOR TO CRIMINAL AND CIVIL LIABILITY. + ******************************************************************************/ +package com.novell.sasl.client; + +import java.util.*; +import org.apache.harmony.javax.security.sasl.*; +import java.io.UnsupportedEncodingException; + +/** + * Implements the DirectiveList class whihc will be used by the + * DigestMD5SaslClient class + */ +class DirectiveList extends Object +{ + private static final int STATE_LOOKING_FOR_FIRST_DIRECTIVE = 1; + private static final int STATE_LOOKING_FOR_DIRECTIVE = 2; + private static final int STATE_SCANNING_NAME = 3; + private static final int STATE_LOOKING_FOR_EQUALS = 4; + private static final int STATE_LOOKING_FOR_VALUE = 5; + private static final int STATE_LOOKING_FOR_COMMA = 6; + private static final int STATE_SCANNING_QUOTED_STRING_VALUE = 7; + private static final int STATE_SCANNING_TOKEN_VALUE = 8; + private static final int STATE_NO_UTF8_SUPPORT = 9; + + private int m_curPos; + private int m_errorPos; + private String m_directives; + private int m_state; + private ArrayList m_directiveList; + private String m_curName; + private int m_scanStart; + + /** + * Constructs a new DirectiveList. + */ + DirectiveList( + byte[] directives) + { + m_curPos = 0; + m_state = STATE_LOOKING_FOR_FIRST_DIRECTIVE; + m_directiveList = new ArrayList(10); + m_scanStart = 0; + m_errorPos = -1; + try + { + m_directives = new String(directives, "UTF-8"); + } + catch(UnsupportedEncodingException e) + { + m_state = STATE_NO_UTF8_SUPPORT; + } + } + + /** + * This function takes a US-ASCII character string containing a list of comma + * separated directives, and parses the string into the individual directives + * and their values. A directive consists of a token specifying the directive + * name followed by an equal sign (=) and the directive value. The value is + * either a token or a quoted string + * + * @exception SaslException If an error Occurs + */ + void parseDirectives() throws SaslException + { + char prevChar; + char currChar; + int rc = 0; + boolean haveQuotedPair = false; + String currentName = "<no name>"; + + if (m_state == STATE_NO_UTF8_SUPPORT) + throw new SaslException("No UTF-8 support on platform"); + + prevChar = 0; + + while (m_curPos < m_directives.length()) + { + currChar = m_directives.charAt(m_curPos); + switch (m_state) + { + case STATE_LOOKING_FOR_FIRST_DIRECTIVE: + case STATE_LOOKING_FOR_DIRECTIVE: + if (isWhiteSpace(currChar)) + { + break; + } + else if (isValidTokenChar(currChar)) + { + m_scanStart = m_curPos; + m_state = STATE_SCANNING_NAME; + } + else + { + m_errorPos = m_curPos; + throw new SaslException("Parse error: Invalid name character"); + } + break; + + case STATE_SCANNING_NAME: + if (isValidTokenChar(currChar)) + { + break; + } + else if (isWhiteSpace(currChar)) + { + currentName = m_directives.substring(m_scanStart, m_curPos); + m_state = STATE_LOOKING_FOR_EQUALS; + } + else if ('=' == currChar) + { + currentName = m_directives.substring(m_scanStart, m_curPos); + m_state = STATE_LOOKING_FOR_VALUE; + } + else + { + m_errorPos = m_curPos; + throw new SaslException("Parse error: Invalid name character"); + } + break; + + case STATE_LOOKING_FOR_EQUALS: + if (isWhiteSpace(currChar)) + { + break; + } + else if ('=' == currChar) + { + m_state = STATE_LOOKING_FOR_VALUE; + } + else + { + m_errorPos = m_curPos; + throw new SaslException("Parse error: Expected equals sign '='."); + } + break; + + case STATE_LOOKING_FOR_VALUE: + if (isWhiteSpace(currChar)) + { + break; + } + else if ('"' == currChar) + { + m_scanStart = m_curPos+1; /* don't include the quote */ + m_state = STATE_SCANNING_QUOTED_STRING_VALUE; + } + else if (isValidTokenChar(currChar)) + { + m_scanStart = m_curPos; + m_state = STATE_SCANNING_TOKEN_VALUE; + } + else + { + m_errorPos = m_curPos; + throw new SaslException("Parse error: Unexpected character"); + } + break; + + case STATE_SCANNING_TOKEN_VALUE: + if (isValidTokenChar(currChar)) + { + break; + } + else if (isWhiteSpace(currChar)) + { + addDirective(currentName, false); + m_state = STATE_LOOKING_FOR_COMMA; + } + else if (',' == currChar) + { + addDirective(currentName, false); + m_state = STATE_LOOKING_FOR_DIRECTIVE; + } + else + { + m_errorPos = m_curPos; + throw new SaslException("Parse error: Invalid value character"); + } + break; + + case STATE_SCANNING_QUOTED_STRING_VALUE: + if ('\\' == currChar) + haveQuotedPair = true; + if ( ('"' == currChar) && + ('\\' != prevChar) ) + { + addDirective(currentName, haveQuotedPair); + haveQuotedPair = false; + m_state = STATE_LOOKING_FOR_COMMA; + } + break; + + case STATE_LOOKING_FOR_COMMA: + if (isWhiteSpace(currChar)) + break; + else if (currChar == ',') + m_state = STATE_LOOKING_FOR_DIRECTIVE; + else + { + m_errorPos = m_curPos; + throw new SaslException("Parse error: Expected a comma."); + } + break; + } + if (0 != rc) + break; + prevChar = currChar; + m_curPos++; + } /* end while loop */ + + + if (rc == 0) + { + /* check the ending state */ + switch (m_state) + { + case STATE_SCANNING_TOKEN_VALUE: + addDirective(currentName, false); + break; + + case STATE_LOOKING_FOR_FIRST_DIRECTIVE: + case STATE_LOOKING_FOR_COMMA: + break; + + case STATE_LOOKING_FOR_DIRECTIVE: + throw new SaslException("Parse error: Trailing comma."); + + case STATE_SCANNING_NAME: + case STATE_LOOKING_FOR_EQUALS: + case STATE_LOOKING_FOR_VALUE: + throw new SaslException("Parse error: Missing value."); + + case STATE_SCANNING_QUOTED_STRING_VALUE: + throw new SaslException("Parse error: Missing closing quote."); + } + } + + } + + /** + * This function returns TRUE if the character is a valid token character. + * + * token = 1*<any CHAR except CTLs or separators> + * + * separators = "(" | ")" | "<" | ">" | "@" + * | "," | ";" | ":" | "\" | <"> + * | "/" | "[" | "]" | "?" | "=" + * | "{" | "}" | SP | HT + * + * CTL = <any US-ASCII control character + * (octets 0 - 31) and DEL (127)> + * + * CHAR = <any US-ASCII character (octets 0 - 127)> + * + * @param c character to be tested + * + * @return Returns TRUE if the character is a valid token character. + */ + boolean isValidTokenChar( + char c) + { + if ( ( (c >= '\u0000') && (c <='\u0020') ) || + ( (c >= '\u003a') && (c <= '\u0040') ) || + ( (c >= '\u005b') && (c <= '\u005d') ) || + ('\u002c' == c) || + ('\u0025' == c) || + ('\u0028' == c) || + ('\u0029' == c) || + ('\u007b' == c) || + ('\u007d' == c) || + ('\u007f' == c) ) + return false; + + return true; + } + + /** + * This function returns TRUE if the character is linear white space (LWS). + * LWS = [CRLF] 1*( SP | HT ) + * @param c Input charcter to be tested + * + * @return Returns TRUE if the character is linear white space (LWS) + */ + boolean isWhiteSpace( + char c) + { + if ( ('\t' == c) || // HORIZONTAL TABULATION. + ('\n' == c) || // LINE FEED. + ('\r' == c) || // CARRIAGE RETURN. + ('\u0020' == c) ) + return true; + + return false; + } + + /** + * This function creates a directive record and adds it to the list, the + * value will be added later after it is parsed. + * + * @param name Name + * @param haveQuotedPair true if quoted pair is there else false + */ + void addDirective( + String name, + boolean haveQuotedPair) + { + String value; + int inputIndex; + int valueIndex; + char valueChar; + int type; + + if (!haveQuotedPair) + { + value = m_directives.substring(m_scanStart, m_curPos); + } + else + { //copy one character at a time skipping backslash excapes. + StringBuffer valueBuf = new StringBuffer(m_curPos - m_scanStart); + valueIndex = 0; + inputIndex = m_scanStart; + while (inputIndex < m_curPos) + { + if ('\\' == (valueChar = m_directives.charAt(inputIndex))) + inputIndex++; + valueBuf.setCharAt(valueIndex, m_directives.charAt(inputIndex)); + valueIndex++; + inputIndex++; + } + value = new String(valueBuf); + } + + if (m_state == STATE_SCANNING_QUOTED_STRING_VALUE) + type = ParsedDirective.QUOTED_STRING_VALUE; + else + type = ParsedDirective.TOKEN_VALUE; + m_directiveList.add(new ParsedDirective(name, value, type)); + } + + + /** + * Returns the List iterator. + * + * @return Returns the Iterator Object for the List. + */ + Iterator getIterator() + { + return m_directiveList.iterator(); + } +} + diff --git a/src/com/novell/sasl/client/ParsedDirective.java b/src/com/novell/sasl/client/ParsedDirective.java new file mode 100644 index 0000000..17bf70e --- /dev/null +++ b/src/com/novell/sasl/client/ParsedDirective.java @@ -0,0 +1,56 @@ +/* ************************************************************************** + * $OpenLDAP: /com/novell/sasl/client/ParsedDirective.java,v 1.1 2003/08/21 10:06:26 kkanil Exp $ + * + * Copyright (C) 2002 Novell, Inc. All Rights Reserved. + * + * THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND + * TREATIES. USE, MODIFICATION, AND REDISTRIBUTION OF THIS WORK IS SUBJECT + * TO VERSION 2.0.1 OF THE OPENLDAP PUBLIC LICENSE, A COPY OF WHICH IS + * AVAILABLE AT HTTP://WWW.OPENLDAP.ORG/LICENSE.HTML OR IN THE FILE "LICENSE" + * IN THE TOP-LEVEL DIRECTORY OF THE DISTRIBUTION. ANY USE OR EXPLOITATION + * OF THIS WORK OTHER THAN AS AUTHORIZED IN VERSION 2.0.1 OF THE OPENLDAP + * PUBLIC LICENSE, OR OTHER PRIOR WRITTEN CONSENT FROM NOVELL, COULD SUBJECT + * THE PERPETRATOR TO CRIMINAL AND CIVIL LIABILITY. + ******************************************************************************/ +package com.novell.sasl.client; + +/** + * Implements the ParsedDirective class which will be used in the + * DigestMD5SaslClient mechanism. + */ +class ParsedDirective +{ + public static final int QUOTED_STRING_VALUE = 1; + public static final int TOKEN_VALUE = 2; + + private int m_valueType; + private String m_name; + private String m_value; + + ParsedDirective( + String name, + String value, + int type) + { + m_name = name; + m_value = value; + m_valueType = type; + } + + String getValue() + { + return m_value; + } + + String getName() + { + return m_name; + } + + int getValueType() + { + return m_valueType; + } + +} + diff --git a/src/com/novell/sasl/client/ResponseAuth.java b/src/com/novell/sasl/client/ResponseAuth.java new file mode 100644 index 0000000..0aef955 --- /dev/null +++ b/src/com/novell/sasl/client/ResponseAuth.java @@ -0,0 +1,83 @@ +/* ************************************************************************** + * $OpenLDAP: /com/novell/sasl/client/ResponseAuth.java,v 1.3 2005/01/17 15:00:54 sunilk Exp $ + * + * Copyright (C) 2002 Novell, Inc. All Rights Reserved. + * + * THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND + * TREATIES. USE, MODIFICATION, AND REDISTRIBUTION OF THIS WORK IS SUBJECT + * TO VERSION 2.0.1 OF THE OPENLDAP PUBLIC LICENSE, A COPY OF WHICH IS + * AVAILABLE AT HTTP://WWW.OPENLDAP.ORG/LICENSE.HTML OR IN THE FILE "LICENSE" + * IN THE TOP-LEVEL DIRECTORY OF THE DISTRIBUTION. ANY USE OR EXPLOITATION + * OF THIS WORK OTHER THAN AS AUTHORIZED IN VERSION 2.0.1 OF THE OPENLDAP + * PUBLIC LICENSE, OR OTHER PRIOR WRITTEN CONSENT FROM NOVELL, COULD SUBJECT + * THE PERPETRATOR TO CRIMINAL AND CIVIL LIABILITY. + ******************************************************************************/ +package com.novell.sasl.client; + +import java.util.*; +import org.apache.harmony.javax.security.sasl.*; + +/** + * Implements the ResponseAuth class used by the DigestMD5SaslClient mechanism + */ +class ResponseAuth +{ + + private String m_responseValue; + + ResponseAuth( + byte[] responseAuth) + throws SaslException + { + m_responseValue = null; + + DirectiveList dirList = new DirectiveList(responseAuth); + try + { + dirList.parseDirectives(); + checkSemantics(dirList); + } + catch (SaslException e) + { + } + } + + /** + * Checks the semantics of the directives in the directive list as parsed + * from the digest challenge byte array. + * + * @param dirList the list of directives parsed from the digest challenge + * + * @exception SaslException If a semantic error occurs + */ + void checkSemantics( + DirectiveList dirList) throws SaslException + { + Iterator directives = dirList.getIterator(); + ParsedDirective directive; + String name; + + while (directives.hasNext()) + { + directive = (ParsedDirective)directives.next(); + name = directive.getName(); + if (name.equals("rspauth")) + m_responseValue = directive.getValue(); + } + + /* post semantic check */ + if (m_responseValue == null) + throw new SaslException("Missing response-auth directive."); + } + + /** + * returns the ResponseValue + * + * @return the ResponseValue as a String. + */ + public String getResponseValue() + { + return m_responseValue; + } +} + diff --git a/src/com/novell/sasl/client/TokenParser.java b/src/com/novell/sasl/client/TokenParser.java new file mode 100644 index 0000000..3d3491d --- /dev/null +++ b/src/com/novell/sasl/client/TokenParser.java @@ -0,0 +1,208 @@ +/* ************************************************************************** + * $OpenLDAP: /com/novell/sasl/client/TokenParser.java,v 1.3 2005/01/17 15:00:54 sunilk Exp $ + * + * Copyright (C) 2002 Novell, Inc. All Rights Reserved. + * + * THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND + * TREATIES. USE, MODIFICATION, AND REDISTRIBUTION OF THIS WORK IS SUBJECT + * TO VERSION 2.0.1 OF THE OPENLDAP PUBLIC LICENSE, A COPY OF WHICH IS + * AVAILABLE AT HTTP://WWW.OPENLDAP.ORG/LICENSE.HTML OR IN THE FILE "LICENSE" + * IN THE TOP-LEVEL DIRECTORY OF THE DISTRIBUTION. ANY USE OR EXPLOITATION + * OF THIS WORK OTHER THAN AS AUTHORIZED IN VERSION 2.0.1 OF THE OPENLDAP + * PUBLIC LICENSE, OR OTHER PRIOR WRITTEN CONSENT FROM NOVELL, COULD SUBJECT + * THE PERPETRATOR TO CRIMINAL AND CIVIL LIABILITY. + ******************************************************************************/ +package com.novell.sasl.client; + +import org.apache.harmony.javax.security.sasl.*; +/** + * The TokenParser class will parse individual tokens from a list of tokens that + * are a directive value for a DigestMD5 authentication.The tokens are separated + * commas. + */ +class TokenParser extends Object +{ + private static final int STATE_LOOKING_FOR_FIRST_TOKEN = 1; + private static final int STATE_LOOKING_FOR_TOKEN = 2; + private static final int STATE_SCANNING_TOKEN = 3; + private static final int STATE_LOOKING_FOR_COMMA = 4; + private static final int STATE_PARSING_ERROR = 5; + private static final int STATE_DONE = 6; + + private int m_curPos; + private int m_scanStart; + private int m_state; + private String m_tokens; + + + TokenParser( + String tokens) + { + m_tokens = tokens; + m_curPos = 0; + m_scanStart = 0; + m_state = STATE_LOOKING_FOR_FIRST_TOKEN; + } + + /** + * This function parses the next token from the tokens string and returns + * it as a string. If there are no more tokens a null reference is returned. + * + * @return the parsed token or a null reference if there are no more + * tokens + * + * @exception SASLException if an error occurs while parsing + */ + String parseToken() throws SaslException + { + char currChar; + String token = null; + + + if (m_state == STATE_DONE) + return null; + + while (m_curPos < m_tokens.length() && (token == null)) + { + currChar = m_tokens.charAt(m_curPos); + switch (m_state) + { + case STATE_LOOKING_FOR_FIRST_TOKEN: + case STATE_LOOKING_FOR_TOKEN: + if (isWhiteSpace(currChar)) + { + break; + } + else if (isValidTokenChar(currChar)) + { + m_scanStart = m_curPos; + m_state = STATE_SCANNING_TOKEN; + } + else + { + m_state = STATE_PARSING_ERROR; + throw new SaslException("Invalid token character at position " + m_curPos); + } + break; + + case STATE_SCANNING_TOKEN: + if (isValidTokenChar(currChar)) + { + break; + } + else if (isWhiteSpace(currChar)) + { + token = m_tokens.substring(m_scanStart, m_curPos); + m_state = STATE_LOOKING_FOR_COMMA; + } + else if (',' == currChar) + { + token = m_tokens.substring(m_scanStart, m_curPos); + m_state = STATE_LOOKING_FOR_TOKEN; + } + else + { + m_state = STATE_PARSING_ERROR; + throw new SaslException("Invalid token character at position " + m_curPos); + } + break; + + + case STATE_LOOKING_FOR_COMMA: + if (isWhiteSpace(currChar)) + break; + else if (currChar == ',') + m_state = STATE_LOOKING_FOR_TOKEN; + else + { + m_state = STATE_PARSING_ERROR; + throw new SaslException("Expected a comma, found '" + + currChar + "' at postion " + + m_curPos); + } + break; + } + m_curPos++; + } /* end while loop */ + + if (token == null) + { /* check the ending state */ + switch (m_state) + { + case STATE_SCANNING_TOKEN: + token = m_tokens.substring(m_scanStart); + m_state = STATE_DONE; + break; + + case STATE_LOOKING_FOR_FIRST_TOKEN: + case STATE_LOOKING_FOR_COMMA: + break; + + case STATE_LOOKING_FOR_TOKEN: + throw new SaslException("Trialing comma"); + } + } + + return token; + } + + /** + * This function returns TRUE if the character is a valid token character. + * + * token = 1*<any CHAR except CTLs or separators> + * + * separators = "(" | ")" | "<" | ">" | "@" + * | "," | ";" | ":" | "\" | <"> + * | "/" | "[" | "]" | "?" | "=" + * | "{" | "}" | SP | HT + * + * CTL = <any US-ASCII control character + * (octets 0 - 31) and DEL (127)> + * + * CHAR = <any US-ASCII character (octets 0 - 127)> + * + * @param c character to be validated + * + * @return True if character is valid Token character else it returns + * false + */ + boolean isValidTokenChar( + char c) + { + if ( ( (c >= '\u0000') && (c <='\u0020') ) || + ( (c >= '\u003a') && (c <= '\u0040') ) || + ( (c >= '\u005b') && (c <= '\u005d') ) || + ('\u002c' == c) || + ('\u0025' == c) || + ('\u0028' == c) || + ('\u0029' == c) || + ('\u007b' == c) || + ('\u007d' == c) || + ('\u007f' == c) ) + return false; + + return true; + } + + /** + * This function returns TRUE if the character is linear white space (LWS). + * LWS = [CRLF] 1*( SP | HT ) + * + * @param c character to be validated + * + * @return True if character is liner whitespace else it returns false + */ + boolean isWhiteSpace( + char c) + { + if ( ('\t' == c) || // HORIZONTAL TABULATION. + ('\n' == c) || // LINE FEED. + ('\r' == c) || // CARRIAGE RETURN. + ('\u0020' == c) ) + return true; + + return false; + } + +} + diff --git a/src/de/measite/smack/AndroidDebugger.java b/src/de/measite/smack/AndroidDebugger.java new file mode 100644 index 0000000..4dfc622 --- /dev/null +++ b/src/de/measite/smack/AndroidDebugger.java @@ -0,0 +1,185 @@ +package de.measite.smack; + +import org.jivesoftware.smack.debugger.SmackDebugger; +import org.jivesoftware.smack.ConnectionListener; +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.util.*; + +import android.util.Log; + +import java.io.Reader; +import java.io.Writer; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * Very simple debugger that prints to the android log the sent and received stanzas. Use + * this debugger with caution since printing to the console is an expensive operation that may + * even block the thread since only one thread may print at a time.<p> + * <p/> + * It is possible to not only print the raw sent and received stanzas but also the interpreted + * packets by Smack. By default interpreted packets won't be printed. To enable this feature + * just change the <tt>printInterpreted</tt> static variable to <tt>true</tt>. + * + * @author Gaston Dombiak + */ +public class AndroidDebugger implements SmackDebugger { + + public static boolean printInterpreted = false; + private SimpleDateFormat dateFormatter = new SimpleDateFormat("hh:mm:ss aaa"); + + private Connection connection = null; + + private PacketListener listener = null; + private ConnectionListener connListener = null; + + private Writer writer; + private Reader reader; + private ReaderListener readerListener; + private WriterListener writerListener; + + public AndroidDebugger(Connection connection, Writer writer, Reader reader) { + this.connection = connection; + this.writer = writer; + this.reader = reader; + createDebug(); + } + + /** + * Creates the listeners that will print in the console when new activity is detected. + */ + private void createDebug() { + // Create a special Reader that wraps the main Reader and logs data to the GUI. + ObservableReader debugReader = new ObservableReader(reader); + readerListener = new ReaderListener() { + public void read(String str) { + Log.d("SMACK", + dateFormatter.format(new Date()) + " RCV (" + connection.hashCode() + + "): " + + str); + } + }; + debugReader.addReaderListener(readerListener); + + // Create a special Writer that wraps the main Writer and logs data to the GUI. + ObservableWriter debugWriter = new ObservableWriter(writer); + writerListener = new WriterListener() { + public void write(String str) { + Log.d("SMACK", + dateFormatter.format(new Date()) + " SENT (" + connection.hashCode() + + "): " + + str); + } + }; + debugWriter.addWriterListener(writerListener); + + // Assign the reader/writer objects to use the debug versions. The packet reader + // and writer will use the debug versions when they are created. + reader = debugReader; + writer = debugWriter; + + // Create a thread that will listen for all incoming packets and write them to + // the GUI. This is what we call "interpreted" packet data, since it's the packet + // data as Smack sees it and not as it's coming in as raw XML. + listener = new PacketListener() { + public void processPacket(Packet packet) { + if (printInterpreted) { + Log.d("SMACK", + dateFormatter.format(new Date()) + " RCV PKT (" + + connection.hashCode() + + "): " + + packet.toXML()); + } + } + }; + + connListener = new ConnectionListener() { + public void connectionClosed() { + Log.d("SMACK", + dateFormatter.format(new Date()) + " Connection closed (" + + connection.hashCode() + + ")"); + } + + public void connectionClosedOnError(Exception e) { + Log.d("SMACK", + dateFormatter.format(new Date()) + + " Connection closed due to an exception (" + + connection.hashCode() + + ")"); + e.printStackTrace(); + } + public void reconnectionFailed(Exception e) { + Log.d("SMACK", + dateFormatter.format(new Date()) + + " Reconnection failed due to an exception (" + + connection.hashCode() + + ")"); + e.printStackTrace(); + } + public void reconnectionSuccessful() { + Log.d("SMACK", + dateFormatter.format(new Date()) + " Connection reconnected (" + + connection.hashCode() + + ")"); + } + public void reconnectingIn(int seconds) { + Log.d("SMACK", + dateFormatter.format(new Date()) + " Connection (" + + connection.hashCode() + + ") will reconnect in " + seconds); + } + }; + } + + public Reader newConnectionReader(Reader newReader) { + ((ObservableReader)reader).removeReaderListener(readerListener); + ObservableReader debugReader = new ObservableReader(newReader); + debugReader.addReaderListener(readerListener); + reader = debugReader; + return reader; + } + + public Writer newConnectionWriter(Writer newWriter) { + ((ObservableWriter)writer).removeWriterListener(writerListener); + ObservableWriter debugWriter = new ObservableWriter(newWriter); + debugWriter.addWriterListener(writerListener); + writer = debugWriter; + return writer; + } + + public void userHasLogged(String user) { + boolean isAnonymous = "".equals(StringUtils.parseName(user)); + String title = + "User logged (" + connection.hashCode() + "): " + + (isAnonymous ? "" : StringUtils.parseBareAddress(user)) + + "@" + + connection.getServiceName() + + ":" + + connection.getPort(); + title += "/" + StringUtils.parseResource(user); + Log.d("SMACK", title); + // Add the connection listener to the connection so that the debugger can be notified + // whenever the connection is closed. + connection.addConnectionListener(connListener); + } + + public Reader getReader() { + return reader; + } + + public Writer getWriter() { + return writer; + } + + public PacketListener getReaderListener() { + return listener; + } + + public PacketListener getWriterListener() { + return null; + } +} + diff --git a/src/de/measite/smack/Sasl.java b/src/de/measite/smack/Sasl.java new file mode 100644 index 0000000..a59135d --- /dev/null +++ b/src/de/measite/smack/Sasl.java @@ -0,0 +1,108 @@ +/* + * Copyright 2009 Rene Treffer + * + * 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. + * + */ +package de.measite.smack; + +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Map; + +import org.apache.harmony.javax.security.auth.callback.CallbackHandler; +import org.apache.harmony.javax.security.sasl.SaslClient; +import org.apache.harmony.javax.security.sasl.SaslException; +import org.apache.harmony.javax.security.sasl.SaslServer; +import org.apache.harmony.javax.security.sasl.SaslServerFactory; + +public class Sasl { + + // SaslClientFactory service name + private static final String CLIENTFACTORYSRV = "SaslClientFactory"; //$NON-NLS-1$ + + // SaslServerFactory service name + private static final String SERVERFACTORYSRV = "SaslServerFactory"; //$NON-NLS-1$ + + public static final String POLICY_NOPLAINTEXT = "javax.security.sasl.policy.noplaintext"; //$NON-NLS-1$ + + public static final String POLICY_NOACTIVE = "javax.security.sasl.policy.noactive"; //$NON-NLS-1$ + + public static final String POLICY_NODICTIONARY = "javax.security.sasl.policy.nodictionary"; //$NON-NLS-1$ + + public static final String POLICY_NOANONYMOUS = "javax.security.sasl.policy.noanonymous"; //$NON-NLS-1$ + + public static final String POLICY_FORWARD_SECRECY = "javax.security.sasl.policy.forward"; //$NON-NLS-1$ + + public static final String POLICY_PASS_CREDENTIALS = "javax.security.sasl.policy.credentials"; //$NON-NLS-1$ + + public static final String MAX_BUFFER = "javax.security.sasl.maxbuffer"; //$NON-NLS-1$ + + public static final String RAW_SEND_SIZE = "javax.security.sasl.rawsendsize"; //$NON-NLS-1$ + + public static final String REUSE = "javax.security.sasl.reuse"; //$NON-NLS-1$ + + public static final String QOP = "javax.security.sasl.qop"; //$NON-NLS-1$ + + public static final String STRENGTH = "javax.security.sasl.strength"; //$NON-NLS-1$ + + public static final String SERVER_AUTH = "javax.security.sasl.server.authentication"; //$NON-NLS-1$ + + public static Enumeration<SaslClientFactory> getSaslClientFactories() { + Hashtable<SaslClientFactory,Object> factories = new Hashtable<SaslClientFactory,Object>(); + factories.put(new SaslClientFactory(), new Object()); + return factories.keys(); + } + + public static Enumeration<SaslServerFactory> getSaslServerFactories() { + return org.apache.harmony.javax.security.sasl.Sasl.getSaslServerFactories(); + } + + public static SaslServer createSaslServer(String mechanism, String protocol, + String serverName, Map<String, ?> prop, CallbackHandler cbh) throws SaslException { + return org.apache.harmony.javax.security.sasl.Sasl.createSaslServer(mechanism, protocol, serverName, prop, cbh); + } + + public static SaslClient createSaslClient(String[] mechanisms, String authanticationID, + String protocol, String serverName, Map<String, ?> prop, CallbackHandler cbh) + throws SaslException { + if (mechanisms == null) { + throw new NullPointerException("auth.33"); //$NON-NLS-1$ + } + SaslClientFactory fact = getSaslClientFactories().nextElement(); + String[] mech = fact.getMechanismNames(null); + boolean is = false; + if (mech != null) { + for (int j = 0; j < mech.length; j++) { + for (int n = 0; n < mechanisms.length; n++) { + if (mech[j].equals(mechanisms[n])) { + is = true; + break; + } + } + } + } + if (is) { + return fact.createSaslClient( + mechanisms, + authanticationID, + protocol, + serverName, + prop, + cbh + ); + } + return null; + } + +} diff --git a/src/de/measite/smack/SaslClientFactory.java b/src/de/measite/smack/SaslClientFactory.java new file mode 100644 index 0000000..2fa1ebd --- /dev/null +++ b/src/de/measite/smack/SaslClientFactory.java @@ -0,0 +1,60 @@ +/* + * Copyright 2009 Rene Treffer + * + * 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. + * + */ +package de.measite.smack; + +import java.util.Map; + +import com.novell.sasl.client.DigestMD5SaslClient; + +import org.apache.harmony.javax.security.auth.callback.CallbackHandler; +import org.apache.harmony.javax.security.sasl.SaslClient; +import org.apache.harmony.javax.security.sasl.SaslException; +import org.apache.qpid.management.common.sasl.PlainSaslClient; + +public class SaslClientFactory implements + org.apache.harmony.javax.security.sasl.SaslClientFactory { + + @Override + public SaslClient createSaslClient(String[] mechanisms, + String authorizationId, String protocol, String serverName, + Map<String, ?> props, CallbackHandler cbh) throws SaslException { + for (String mech: mechanisms) { + if ("PLAIN".equals(mech)) { + return new PlainSaslClient(authorizationId, cbh); + } else + if ("DIGEST-MD5".equals(mech)) { + return DigestMD5SaslClient.getClient( + authorizationId, + protocol, + serverName, + props, + cbh + ); + } + } + return null; + } + + @Override + public String[] getMechanismNames(Map<String, ?> props) { + return new String[]{ + "PLAIN", + "DIGEST-MD5" + }; + } + +} diff --git a/src/org/apache/harmony/javax/security/auth/AuthPermission.java b/src/org/apache/harmony/javax/security/auth/AuthPermission.java new file mode 100644 index 0000000..bb12554 --- /dev/null +++ b/src/org/apache/harmony/javax/security/auth/AuthPermission.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +package org.apache.harmony.javax.security.auth; + +import java.security.BasicPermission; + + + +/** + * Governs the use of methods in this package and also its subpackages. A + * <i>target name</i> of the permission specifies which methods are allowed + * without specifying the concrete action lists. Possible target names and + * associated authentication permissions are: + * + * <pre> + * doAs invoke Subject.doAs methods. + * doAsPrivileged invoke the Subject.doAsPrivileged methods. + * getSubject invoke Subject.getSubject(). + * getSubjectFromDomainCombiner invoke SubjectDomainCombiner.getSubject(). + * setReadOnly invoke Subject.setReadonly(). + * modifyPrincipals modify the set of principals + * associated with a Subject. + * modifyPublicCredentials modify the set of public credentials + * associated with a Subject. + * modifyPrivateCredentials modify the set of private credentials + * associated with a Subject. + * refreshCredential invoke the refresh method on a credential of a + * refreshable credential class. + * destroyCredential invoke the destroy method on a credential of a + * destroyable credential class. + * createLoginContext.<i>name</i> instantiate a LoginContext with the + * specified name. The wildcard name ('*') + * allows to a LoginContext of any name. + * getLoginConfiguration invoke the getConfiguration method of + * javax.security.auth.login.Configuration. + * refreshLoginConfiguration Invoke the refresh method of + * javax.security.auth.login.Configuration. + * </pre> + */ +public final class AuthPermission extends BasicPermission { + + private static final long serialVersionUID = 5806031445061587174L; + + private static final String CREATE_LOGIN_CONTEXT = "createLoginContext"; //$NON-NLS-1$ + + private static final String CREATE_LOGIN_CONTEXT_ANY = "createLoginContext.*"; //$NON-NLS-1$ + + // inits permission name. + private static String init(String name) { + + if (name == null) { + throw new NullPointerException("auth.13"); //$NON-NLS-1$ + } + + if (CREATE_LOGIN_CONTEXT.equals(name)) { + return CREATE_LOGIN_CONTEXT_ANY; + } + return name; + } + + /** + * Creates an authentication permission with the specified target name. + * + * @param name + * the target name of this authentication permission. + */ + public AuthPermission(String name) { + super(init(name)); + } + + /** + * Creates an authentication permission with the specified target name. + * + * @param name + * the target name of this authentication permission. + * @param actions + * this parameter is ignored and should be {@code null}. + */ + public AuthPermission(String name, String actions) { + super(init(name), actions); + } +}
\ No newline at end of file diff --git a/src/org/apache/harmony/javax/security/auth/DestroyFailedException.java b/src/org/apache/harmony/javax/security/auth/DestroyFailedException.java new file mode 100644 index 0000000..7c7ea79 --- /dev/null +++ b/src/org/apache/harmony/javax/security/auth/DestroyFailedException.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +package org.apache.harmony.javax.security.auth; + +/** + * Signals that the {@link Destroyable#destroy()} method failed. + */ +public class DestroyFailedException extends Exception { + + private static final long serialVersionUID = -7790152857282749162L; + + /** + * Creates an exception of type {@code DestroyFailedException}. + */ + public DestroyFailedException() { + super(); + } + + /** + * Creates an exception of type {@code DestroyFailedException}. + * + * @param message + * A detail message that describes the reason for this exception. + */ + public DestroyFailedException(String message) { + super(message); + } + +} diff --git a/src/org/apache/harmony/javax/security/auth/Destroyable.java b/src/org/apache/harmony/javax/security/auth/Destroyable.java new file mode 100644 index 0000000..12a107b --- /dev/null +++ b/src/org/apache/harmony/javax/security/auth/Destroyable.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +package org.apache.harmony.javax.security.auth; + +/** + * Allows for special treatment of sensitive information, when it comes to + * destroying or clearing of the data. + */ +public interface Destroyable { + + /** + * Erases the sensitive information. Once an object is destroyed any calls + * to its methods will throw an {@code IllegalStateException}. If it does + * not succeed a DestroyFailedException is thrown. + * + * @throws DestroyFailedException + * if the information cannot be erased. + */ + void destroy() throws DestroyFailedException; + + /** + * Returns {@code true} once an object has been safely destroyed. + * + * @return whether the object has been safely destroyed. + */ + boolean isDestroyed(); + +} diff --git a/src/org/apache/harmony/javax/security/auth/PrivateCredentialPermission.java b/src/org/apache/harmony/javax/security/auth/PrivateCredentialPermission.java new file mode 100644 index 0000000..d62bb24 --- /dev/null +++ b/src/org/apache/harmony/javax/security/auth/PrivateCredentialPermission.java @@ -0,0 +1,395 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +package org.apache.harmony.javax.security.auth; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.security.Permission; +import java.security.PermissionCollection; +import java.security.Principal; +import java.util.Set; + + + +/** + * Protects private credential objects belonging to a {@code Subject}. It has + * only one action which is "read". The target name of this permission has a + * special syntax: + * + * <pre> + * targetName = CredentialClass {PrincipalClass "PrincipalName"}* + * </pre> + * + * First it states a credential class and is followed then by a list of one or + * more principals identifying the subject. + * <p> + * The principals on their part are specified as the name of the {@code + * Principal} class followed by the principal name in quotes. For example, the + * following file may define permission to read the private credentials of a + * principal named "Bob": "com.sun.PrivateCredential com.sun.Principal \"Bob\"" + * <p> + * The syntax also allows the use of the wildcard "*" in place of {@code + * CredentialClass} or {@code PrincipalClass} and/or {@code PrincipalName}. + * + * @see Principal + */ +public final class PrivateCredentialPermission extends Permission { + + private static final long serialVersionUID = 5284372143517237068L; + + // allowed action + private static final String READ = "read"; //$NON-NLS-1$ + + private String credentialClass; + + // current offset + private transient int offset; + + // owners set + private transient CredOwner[] set; + + /** + * Creates a new permission for private credentials specified by the target + * name {@code name} and an {@code action}. The action is always + * {@code "read"}. + * + * @param name + * the target name of the permission. + * @param action + * the action {@code "read"}. + */ + public PrivateCredentialPermission(String name, String action) { + super(name); + if (READ.equalsIgnoreCase(action)) { + initTargetName(name); + } else { + throw new IllegalArgumentException("auth.11"); //$NON-NLS-1$ + } + } + + /** + * Creates a {@code PrivateCredentialPermission} from the {@code Credential} + * class and set of principals. + * + * @param credentialClass + * the credential class name. + * @param principals + * the set of principals. + */ + PrivateCredentialPermission(String credentialClass, Set<Principal> principals) { + super(credentialClass); + this.credentialClass = credentialClass; + + set = new CredOwner[principals.size()]; + for (Principal p : principals) { + CredOwner element = new CredOwner(p.getClass().getName(), p.getName()); + // check for duplicate elements + boolean found = false; + for (int ii = 0; ii < offset; ii++) { + if (set[ii].equals(element)) { + found = true; + break; + } + } + if (!found) { + set[offset++] = element; + } + } + } + + /** + * Initialize a PrivateCredentialPermission object and checks that a target + * name has a correct format: CredentialClass 1*(PrincipalClass + * "PrincipalName") + */ + private void initTargetName(String name) { + + if (name == null) { + throw new NullPointerException("auth.0E"); //$NON-NLS-1$ + } + + // check empty string + name = name.trim(); + if (name.length() == 0) { + throw new IllegalArgumentException("auth.0F"); //$NON-NLS-1$ + } + + // get CredentialClass + int beg = name.indexOf(' '); + if (beg == -1) { + throw new IllegalArgumentException("auth.10"); //$NON-NLS-1$ + } + credentialClass = name.substring(0, beg); + + // get a number of pairs: PrincipalClass "PrincipalName" + beg++; + int count = 0; + int nameLength = name.length(); + for (int i, j = 0; beg < nameLength; beg = j + 2, count++) { + i = name.indexOf(' ', beg); + j = name.indexOf('"', i + 2); + + if (i == -1 || j == -1 || name.charAt(i + 1) != '"') { + throw new IllegalArgumentException("auth.10"); //$NON-NLS-1$ + } + } + + // name MUST have one pair at least + if (count < 1) { + throw new IllegalArgumentException("auth.10"); //$NON-NLS-1$ + } + + beg = name.indexOf(' '); + beg++; + + // populate principal set with instances of CredOwner class + String principalClass; + String principalName; + + set = new CredOwner[count]; + for (int index = 0, i, j; index < count; beg = j + 2, index++) { + i = name.indexOf(' ', beg); + j = name.indexOf('"', i + 2); + + principalClass = name.substring(beg, i); + principalName = name.substring(i + 2, j); + + CredOwner element = new CredOwner(principalClass, principalName); + // check for duplicate elements + boolean found = false; + for (int ii = 0; ii < offset; ii++) { + if (set[ii].equals(element)) { + found = true; + break; + } + } + if (!found) { + set[offset++] = element; + } + } + } + + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + ois.defaultReadObject(); + initTargetName(getName()); + } + + /** + * Returns the principal's classes and names associated with this {@code + * PrivateCredentialPermission} as a two dimensional array. The first + * dimension of the array corresponds to the number of principals. The + * second dimension defines either the name of the {@code PrincipalClass} + * [x][0] or the value of {@code PrincipalName} [x][1]. + * <p> + * This corresponds to the the target name's syntax: + * + * <pre> + * targetName = CredentialClass {PrincipalClass "PrincipalName"}* + * </pre> + * + * @return the principal classes and names associated with this {@code + * PrivateCredentialPermission}. + */ + public String[][] getPrincipals() { + + String[][] s = new String[offset][2]; + + for (int i = 0; i < s.length; i++) { + s[i][0] = set[i].principalClass; + s[i][1] = set[i].principalName; + } + return s; + } + + @Override + public String getActions() { + return READ; + } + + /** + * Returns the class name of the credential associated with this permission. + * + * @return the class name of the credential associated with this permission. + */ + public String getCredentialClass() { + return credentialClass; + } + + @Override + public int hashCode() { + int hash = 0; + for (int i = 0; i < offset; i++) { + hash = hash + set[i].hashCode(); + } + return getCredentialClass().hashCode() + hash; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (obj == null || this.getClass() != obj.getClass()) { + return false; + } + + PrivateCredentialPermission that = (PrivateCredentialPermission) obj; + + return credentialClass.equals(that.credentialClass) && (offset == that.offset) + && sameMembers(set, that.set, offset); + } + + @Override + public boolean implies(Permission permission) { + + if (permission == null || this.getClass() != permission.getClass()) { + return false; + } + + PrivateCredentialPermission that = (PrivateCredentialPermission) permission; + + if (!("*".equals(credentialClass) || credentialClass //$NON-NLS-1$ + .equals(that.getCredentialClass()))) { + return false; + } + + if (that.offset == 0) { + return true; + } + + CredOwner[] thisCo = set; + CredOwner[] thatCo = that.set; + int thisPrincipalsSize = offset; + int thatPrincipalsSize = that.offset; + for (int i = 0, j; i < thisPrincipalsSize; i++) { + for (j = 0; j < thatPrincipalsSize; j++) { + if (thisCo[i].implies(thatCo[j])) { + break; + } + } + if (j == thatCo.length) { + return false; + } + } + return true; + } + + @Override + public PermissionCollection newPermissionCollection() { + return null; + } + + /** + * Returns true if the two arrays have the same length, and every member of + * one array is contained in another array + */ + private boolean sameMembers(Object[] ar1, Object[] ar2, int length) { + if (ar1 == null && ar2 == null) { + return true; + } + if (ar1 == null || ar2 == null) { + return false; + } + boolean found; + for (int i = 0; i < length; i++) { + found = false; + for (int j = 0; j < length; j++) { + if (ar1[i].equals(ar2[j])) { + found = true; + break; + } + } + if (!found) { + return false; + } + } + return true; + } + + private static final class CredOwner implements Serializable { + + private static final long serialVersionUID = -5607449830436408266L; + + String principalClass; + + String principalName; + + // whether class name contains wildcards + private transient boolean isClassWildcard; + + // whether pname contains wildcards + private transient boolean isPNameWildcard; + + // Creates a new CredOwner with the specified Principal Class and Principal Name + CredOwner(String principalClass, String principalName) { + super(); + if ("*".equals(principalClass)) { //$NON-NLS-1$ + isClassWildcard = true; + } + + if ("*".equals(principalName)) { //$NON-NLS-1$ + isPNameWildcard = true; + } + + if (isClassWildcard && !isPNameWildcard) { + throw new IllegalArgumentException("auth.12"); //$NON-NLS-1$ + } + + this.principalClass = principalClass; + this.principalName = principalName; + } + + // Checks if this CredOwner implies the specified Object. + boolean implies(Object obj) { + if (obj == this) { + return true; + } + + CredOwner co = (CredOwner) obj; + + if (isClassWildcard || principalClass.equals(co.principalClass)) { + if (isPNameWildcard || principalName.equals(co.principalName)) { + return true; + } + } + return false; + } + + // Checks two CredOwner objects for equality. + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof CredOwner) { + CredOwner that = (CredOwner) obj; + return principalClass.equals(that.principalClass) + && principalName.equals(that.principalName); + } + return false; + } + + // Returns the hash code value for this object. + @Override + public int hashCode() { + return principalClass.hashCode() + principalName.hashCode(); + } + } +} diff --git a/src/org/apache/harmony/javax/security/auth/RefreshFailedException.java b/src/org/apache/harmony/javax/security/auth/RefreshFailedException.java new file mode 100644 index 0000000..71bcc6b --- /dev/null +++ b/src/org/apache/harmony/javax/security/auth/RefreshFailedException.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +package org.apache.harmony.javax.security.auth; + +public class RefreshFailedException extends Exception { + + private static final long serialVersionUID = 5058444488565265840L; + + public RefreshFailedException() { + super(); + } + + public RefreshFailedException(String message) { + super(message); + } +} diff --git a/src/org/apache/harmony/javax/security/auth/Refreshable.java b/src/org/apache/harmony/javax/security/auth/Refreshable.java new file mode 100644 index 0000000..90b00cb --- /dev/null +++ b/src/org/apache/harmony/javax/security/auth/Refreshable.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +package org.apache.harmony.javax.security.auth; + +public interface Refreshable { + + void refresh() throws RefreshFailedException; + + boolean isCurrent(); + +} diff --git a/src/org/apache/harmony/javax/security/auth/Subject.java b/src/org/apache/harmony/javax/security/auth/Subject.java new file mode 100644 index 0000000..142686e --- /dev/null +++ b/src/org/apache/harmony/javax/security/auth/Subject.java @@ -0,0 +1,782 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +package org.apache.harmony.javax.security.auth; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.security.AccessControlContext; +import java.security.AccessController; +import java.security.DomainCombiner; +import java.security.Permission; +import java.security.Principal; +import java.security.PrivilegedAction; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.security.ProtectionDomain; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Set; + + + +/** + * The central class of the {@code javax.security.auth} package representing an + * authenticated user or entity (both referred to as "subject"). IT defines also + * the static methods that allow code to be run, and do modifications according + * to the subject's permissions. + * <p> + * A subject has the following features: + * <ul> + * <li>A set of {@code Principal} objects specifying the identities bound to a + * {@code Subject} that distinguish it.</li> + * <li>Credentials (public and private) such as certificates, keys, or + * authentication proofs such as tickets</li> + * </ul> + */ +public final class Subject implements Serializable { + + private static final long serialVersionUID = -8308522755600156056L; + + private static final AuthPermission _AS = new AuthPermission("doAs"); //$NON-NLS-1$ + + private static final AuthPermission _AS_PRIVILEGED = new AuthPermission( + "doAsPrivileged"); //$NON-NLS-1$ + + private static final AuthPermission _SUBJECT = new AuthPermission( + "getSubject"); //$NON-NLS-1$ + + private static final AuthPermission _PRINCIPALS = new AuthPermission( + "modifyPrincipals"); //$NON-NLS-1$ + + private static final AuthPermission _PRIVATE_CREDENTIALS = new AuthPermission( + "modifyPrivateCredentials"); //$NON-NLS-1$ + + private static final AuthPermission _PUBLIC_CREDENTIALS = new AuthPermission( + "modifyPublicCredentials"); //$NON-NLS-1$ + + private static final AuthPermission _READ_ONLY = new AuthPermission( + "setReadOnly"); //$NON-NLS-1$ + + private final Set<Principal> principals; + + private boolean readOnly; + + // set of private credentials + private transient SecureSet<Object> privateCredentials; + + // set of public credentials + private transient SecureSet<Object> publicCredentials; + + /** + * The default constructor initializing the sets of public and private + * credentials and principals with the empty set. + */ + public Subject() { + super(); + principals = new SecureSet<Principal>(_PRINCIPALS); + publicCredentials = new SecureSet<Object>(_PUBLIC_CREDENTIALS); + privateCredentials = new SecureSet<Object>(_PRIVATE_CREDENTIALS); + + readOnly = false; + } + + /** + * The constructor for the subject, setting its public and private + * credentials and principals according to the arguments. + * + * @param readOnly + * {@code true} if this {@code Subject} is read-only, thus + * preventing any modifications to be done. + * @param subjPrincipals + * the set of Principals that are attributed to this {@code + * Subject}. + * @param pubCredentials + * the set of public credentials that distinguish this {@code + * Subject}. + * @param privCredentials + * the set of private credentials that distinguish this {@code + * Subject}. + */ + public Subject(boolean readOnly, Set<? extends Principal> subjPrincipals, + Set<?> pubCredentials, Set<?> privCredentials) { + + if (subjPrincipals == null || pubCredentials == null || privCredentials == null) { + throw new NullPointerException(); + } + + principals = new SecureSet<Principal>(_PRINCIPALS, subjPrincipals); + publicCredentials = new SecureSet<Object>(_PUBLIC_CREDENTIALS, pubCredentials); + privateCredentials = new SecureSet<Object>(_PRIVATE_CREDENTIALS, privCredentials); + + this.readOnly = readOnly; + } + + /** + * Runs the code defined by {@code action} using the permissions granted to + * the {@code Subject} itself and to the code as well. + * + * @param subject + * the distinguished {@code Subject}. + * @param action + * the code to be run. + * @return the {@code Object} returned when running the {@code action}. + */ + @SuppressWarnings("unchecked") + public static Object doAs(Subject subject, PrivilegedAction action) { + + checkPermission(_AS); + + return doAs_PrivilegedAction(subject, action, AccessController.getContext()); + } + + /** + * Run the code defined by {@code action} using the permissions granted to + * the {@code Subject} and to the code itself, additionally providing a more + * specific context. + * + * @param subject + * the distinguished {@code Subject}. + * @param action + * the code to be run. + * @param context + * the specific context in which the {@code action} is invoked. + * if {@code null} a new {@link AccessControlContext} is + * instantiated. + * @return the {@code Object} returned when running the {@code action}. + */ + @SuppressWarnings("unchecked") + public static Object doAsPrivileged(Subject subject, PrivilegedAction action, + AccessControlContext context) { + + checkPermission(_AS_PRIVILEGED); + + if (context == null) { + return doAs_PrivilegedAction(subject, action, new AccessControlContext( + new ProtectionDomain[0])); + } + return doAs_PrivilegedAction(subject, action, context); + } + + // instantiates a new context and passes it to AccessController + @SuppressWarnings("unchecked") + private static Object doAs_PrivilegedAction(Subject subject, PrivilegedAction action, + final AccessControlContext context) { + + AccessControlContext newContext; + + final SubjectDomainCombiner combiner; + if (subject == null) { + // performance optimization + // if subject is null there is nothing to combine + combiner = null; + } else { + combiner = new SubjectDomainCombiner(subject); + } + + PrivilegedAction dccAction = new PrivilegedAction() { + public Object run() { + + return new AccessControlContext(context, combiner); + } + }; + + newContext = (AccessControlContext) AccessController.doPrivileged(dccAction); + + return AccessController.doPrivileged(action, newContext); + } + + /** + * Runs the code defined by {@code action} using the permissions granted to + * the subject and to the code itself. + * + * @param subject + * the distinguished {@code Subject}. + * @param action + * the code to be run. + * @return the {@code Object} returned when running the {@code action}. + * @throws PrivilegedActionException + * if running the {@code action} throws an exception. + */ + @SuppressWarnings("unchecked") + public static Object doAs(Subject subject, PrivilegedExceptionAction action) + throws PrivilegedActionException { + + checkPermission(_AS); + + return doAs_PrivilegedExceptionAction(subject, action, AccessController.getContext()); + } + + /** + * Runs the code defined by {@code action} using the permissions granted to + * the subject and to the code itself, additionally providing a more + * specific context. + * + * @param subject + * the distinguished {@code Subject}. + * @param action + * the code to be run. + * @param context + * the specific context in which the {@code action} is invoked. + * if {@code null} a new {@link AccessControlContext} is + * instantiated. + * @return the {@code Object} returned when running the {@code action}. + * @throws PrivilegedActionException + * if running the {@code action} throws an exception. + */ + @SuppressWarnings("unchecked") + public static Object doAsPrivileged(Subject subject, + PrivilegedExceptionAction action, AccessControlContext context) + throws PrivilegedActionException { + + checkPermission(_AS_PRIVILEGED); + + if (context == null) { + return doAs_PrivilegedExceptionAction(subject, action, + new AccessControlContext(new ProtectionDomain[0])); + } + return doAs_PrivilegedExceptionAction(subject, action, context); + } + + // instantiates a new context and passes it to AccessController + @SuppressWarnings("unchecked") + private static Object doAs_PrivilegedExceptionAction(Subject subject, + PrivilegedExceptionAction action, final AccessControlContext context) + throws PrivilegedActionException { + + AccessControlContext newContext; + + final SubjectDomainCombiner combiner; + if (subject == null) { + // performance optimization + // if subject is null there is nothing to combine + combiner = null; + } else { + combiner = new SubjectDomainCombiner(subject); + } + + PrivilegedAction<AccessControlContext> dccAction = new PrivilegedAction<AccessControlContext>() { + public AccessControlContext run() { + return new AccessControlContext(context, combiner); + } + }; + + newContext = AccessController.doPrivileged(dccAction); + + return AccessController.doPrivileged(action, newContext); + } + + /** + * Checks two Subjects for equality. More specifically if the principals, + * public and private credentials are equal, equality for two {@code + * Subjects} is implied. + * + * @param obj + * the {@code Object} checked for equality with this {@code + * Subject}. + * @return {@code true} if the specified {@code Subject} is equal to this + * one. + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (obj == null || this.getClass() != obj.getClass()) { + return false; + } + + Subject that = (Subject) obj; + + if (principals.equals(that.principals) + && publicCredentials.equals(that.publicCredentials) + && privateCredentials.equals(that.privateCredentials)) { + return true; + } + return false; + } + + /** + * Returns this {@code Subject}'s {@link Principal}. + * + * @return this {@code Subject}'s {@link Principal}. + */ + public Set<Principal> getPrincipals() { + return principals; + } + + + /** + * Returns this {@code Subject}'s {@link Principal} which is a subclass of + * the {@code Class} provided. + * + * @param c + * the {@code Class} as a criteria which the {@code Principal} + * returned must satisfy. + * @return this {@code Subject}'s {@link Principal}. Modifications to the + * returned set of {@code Principal}s do not affect this {@code + * Subject}'s set. + */ + public <T extends Principal> Set<T> getPrincipals(Class<T> c) { + return ((SecureSet<Principal>) principals).get(c); + } + + /** + * Returns the private credentials associated with this {@code Subject}. + * + * @return the private credentials associated with this {@code Subject}. + */ + public Set<Object> getPrivateCredentials() { + return privateCredentials; + } + + /** + * Returns this {@code Subject}'s private credentials which are a subclass + * of the {@code Class} provided. + * + * @param c + * the {@code Class} as a criteria which the private credentials + * returned must satisfy. + * @return this {@code Subject}'s private credentials. Modifications to the + * returned set of credentials do not affect this {@code Subject}'s + * credentials. + */ + public <T> Set<T> getPrivateCredentials(Class<T> c) { + return privateCredentials.get(c); + } + + /** + * Returns the public credentials associated with this {@code Subject}. + * + * @return the public credentials associated with this {@code Subject}. + */ + public Set<Object> getPublicCredentials() { + return publicCredentials; + } + + + /** + * Returns this {@code Subject}'s public credentials which are a subclass of + * the {@code Class} provided. + * + * @param c + * the {@code Class} as a criteria which the public credentials + * returned must satisfy. + * @return this {@code Subject}'s public credentials. Modifications to the + * returned set of credentials do not affect this {@code Subject}'s + * credentials. + */ + public <T> Set<T> getPublicCredentials(Class<T> c) { + return publicCredentials.get(c); + } + + /** + * Returns a hash code of this {@code Subject}. + * + * @return a hash code of this {@code Subject}. + */ + @Override + public int hashCode() { + return principals.hashCode() + privateCredentials.hashCode() + + publicCredentials.hashCode(); + } + + /** + * Prevents from modifications being done to the credentials and {@link + * Principal} sets. After setting it to read-only this {@code Subject} can + * not be made writable again. The destroy method on the credentials still + * works though. + */ + public void setReadOnly() { + checkPermission(_READ_ONLY); + + readOnly = true; + } + + /** + * Returns whether this {@code Subject} is read-only or not. + * + * @return whether this {@code Subject} is read-only or not. + */ + public boolean isReadOnly() { + return readOnly; + } + + /** + * Returns a {@code String} representation of this {@code Subject}. + * + * @return a {@code String} representation of this {@code Subject}. + */ + @Override + public String toString() { + + StringBuilder buf = new StringBuilder("Subject:\n"); //$NON-NLS-1$ + + Iterator<?> it = principals.iterator(); + while (it.hasNext()) { + buf.append("\tPrincipal: "); //$NON-NLS-1$ + buf.append(it.next()); + buf.append('\n'); + } + + it = publicCredentials.iterator(); + while (it.hasNext()) { + buf.append("\tPublic Credential: "); //$NON-NLS-1$ + buf.append(it.next()); + buf.append('\n'); + } + + int offset = buf.length() - 1; + it = privateCredentials.iterator(); + try { + while (it.hasNext()) { + buf.append("\tPrivate Credential: "); //$NON-NLS-1$ + buf.append(it.next()); + buf.append('\n'); + } + } catch (SecurityException e) { + buf.delete(offset, buf.length()); + buf.append("\tPrivate Credentials: no accessible information\n"); //$NON-NLS-1$ + } + return buf.toString(); + } + + private void readObject(ObjectInputStream in) throws IOException, + ClassNotFoundException { + + in.defaultReadObject(); + + publicCredentials = new SecureSet<Object>(_PUBLIC_CREDENTIALS); + privateCredentials = new SecureSet<Object>(_PRIVATE_CREDENTIALS); + } + + private void writeObject(ObjectOutputStream out) throws IOException { + out.defaultWriteObject(); + } + + /** + * Returns the {@code Subject} that was last associated with the {@code + * context} provided as argument. + * + * @param context + * the {@code context} that was associated with the + * {@code Subject}. + * @return the {@code Subject} that was last associated with the {@code + * context} provided as argument. + */ + public static Subject getSubject(final AccessControlContext context) { + checkPermission(_SUBJECT); + if (context == null) { + throw new NullPointerException("auth.09"); //$NON-NLS-1$ + } + PrivilegedAction<DomainCombiner> action = new PrivilegedAction<DomainCombiner>() { + public DomainCombiner run() { + return context.getDomainCombiner(); + } + }; + DomainCombiner combiner = AccessController.doPrivileged(action); + + if ((combiner == null) || !(combiner instanceof SubjectDomainCombiner)) { + return null; + } + return ((SubjectDomainCombiner) combiner).getSubject(); + } + + // checks passed permission + private static void checkPermission(Permission p) { + SecurityManager sm = System.getSecurityManager(); + if (sm != null) { + sm.checkPermission(p); + } + } + + // FIXME is used only in two places. remove? + private void checkState() { + if (readOnly) { + throw new IllegalStateException("auth.0A"); //$NON-NLS-1$ + } + } + + private final class SecureSet<SST> extends AbstractSet<SST> implements Serializable { + + /** + * Compatibility issue: see comments for setType variable + */ + private static final long serialVersionUID = 7911754171111800359L; + + private LinkedList<SST> elements; + + /* + * Is used to define a set type for serialization. + * + * A type can be principal, priv. or pub. credential set. The spec. + * doesn't clearly says that priv. and pub. credential sets can be + * serialized and what classes they are. It is only possible to figure + * out from writeObject method comments that priv. credential set is + * serializable and it is an instance of SecureSet class. So pub. + * credential was implemented by analogy + * + * Compatibility issue: the class follows its specified serial form. + * Also according to the serialization spec. adding new field is a + * compatible change. So is ok for principal set (because the default + * value for integer is zero). But priv. or pub. credential set it is + * not compatible because most probably other implementations resolve + * this issue in other way + */ + private int setType; + + // Defines principal set for serialization. + private static final int SET_Principal = 0; + + // Defines private credential set for serialization. + private static final int SET_PrivCred = 1; + + // Defines public credential set for serialization. + private static final int SET_PubCred = 2; + + // permission required to modify set + private transient AuthPermission permission; + + protected SecureSet(AuthPermission perm) { + permission = perm; + elements = new LinkedList<SST>(); + } + + // creates set from specified collection with specified permission + // all collection elements are verified before adding + protected SecureSet(AuthPermission perm, Collection<? extends SST> s) { + this(perm); + + // Subject's constructor receives a Set, we can trusts if a set is from bootclasspath, + // and not to check whether it contains duplicates or not + boolean trust = s.getClass().getClassLoader() == null; + + Iterator<? extends SST> it = s.iterator(); + while (it.hasNext()) { + SST o = it.next(); + verifyElement(o); + if (trust || !elements.contains(o)) { + elements.add(o); + } + } + } + + // verifies new set element + private void verifyElement(Object o) { + + if (o == null) { + throw new NullPointerException(); + } + if (permission == _PRINCIPALS && !(Principal.class.isAssignableFrom(o.getClass()))) { + throw new IllegalArgumentException("auth.0B"); //$NON-NLS-1$ + } + } + + /* + * verifies specified element, checks set state, and security permission + * to modify set before adding new element + */ + @Override + public boolean add(SST o) { + + verifyElement(o); + + checkState(); + checkPermission(permission); + + if (!elements.contains(o)) { + elements.add(o); + return true; + } + return false; + } + + // returns an instance of SecureIterator + @Override + public Iterator<SST> iterator() { + + if (permission == _PRIVATE_CREDENTIALS) { + /* + * private credential set requires iterator with additional + * security check (PrivateCredentialPermission) + */ + return new SecureIterator(elements.iterator()) { + /* + * checks permission to access next private credential moves + * to the next element even SecurityException was thrown + */ + @Override + public SST next() { + SST obj = iterator.next(); + checkPermission(new PrivateCredentialPermission(obj + .getClass().getName(), principals)); + return obj; + } + }; + } + return new SecureIterator(elements.iterator()); + } + + @Override + public boolean retainAll(Collection<?> c) { + + if (c == null) { + throw new NullPointerException(); + } + return super.retainAll(c); + } + + @Override + public int size() { + return elements.size(); + } + + /** + * return set with elements that are instances or subclasses of the + * specified class + */ + protected final <E> Set<E> get(final Class<E> c) { + + if (c == null) { + throw new NullPointerException(); + } + + AbstractSet<E> s = new AbstractSet<E>() { + private LinkedList<E> elements = new LinkedList<E>(); + + @Override + public boolean add(E o) { + + if (!c.isAssignableFrom(o.getClass())) { + throw new IllegalArgumentException( + "auth.0C " + c.getName()); //$NON-NLS-1$ + } + + if (elements.contains(o)) { + return false; + } + elements.add(o); + return true; + } + + @Override + public Iterator<E> iterator() { + return elements.iterator(); + } + + @Override + public boolean retainAll(Collection<?> c) { + + if (c == null) { + throw new NullPointerException(); + } + return super.retainAll(c); + } + + @Override + public int size() { + return elements.size(); + } + }; + + // FIXME must have permissions for requested priv. credentials + for (Iterator<SST> it = iterator(); it.hasNext();) { + SST o = it.next(); + if (c.isAssignableFrom(o.getClass())) { + s.add(c.cast(o)); + } + } + return s; + } + + private void readObject(ObjectInputStream in) throws IOException, + ClassNotFoundException { + in.defaultReadObject(); + + switch (setType) { + case SET_Principal: + permission = _PRINCIPALS; + break; + case SET_PrivCred: + permission = _PRIVATE_CREDENTIALS; + break; + case SET_PubCred: + permission = _PUBLIC_CREDENTIALS; + break; + default: + throw new IllegalArgumentException(); + } + + Iterator<SST> it = elements.iterator(); + while (it.hasNext()) { + verifyElement(it.next()); + } + } + + private void writeObject(ObjectOutputStream out) throws IOException { + + if (permission == _PRIVATE_CREDENTIALS) { + // does security check for each private credential + for (Iterator<SST> it = iterator(); it.hasNext();) { + it.next(); + } + setType = SET_PrivCred; + } else if (permission == _PRINCIPALS) { + setType = SET_Principal; + } else { + setType = SET_PubCred; + } + + out.defaultWriteObject(); + } + + /** + * Represents iterator for subject's secure set + */ + private class SecureIterator implements Iterator<SST> { + protected Iterator<SST> iterator; + + protected SecureIterator(Iterator<SST> iterator) { + this.iterator = iterator; + } + + public boolean hasNext() { + return iterator.hasNext(); + } + + public SST next() { + return iterator.next(); + } + + /** + * checks set state, and security permission to modify set before + * removing current element + */ + public void remove() { + checkState(); + checkPermission(permission); + iterator.remove(); + } + } + } +} diff --git a/src/org/apache/harmony/javax/security/auth/SubjectDomainCombiner.java b/src/org/apache/harmony/javax/security/auth/SubjectDomainCombiner.java new file mode 100644 index 0000000..edbb672 --- /dev/null +++ b/src/org/apache/harmony/javax/security/auth/SubjectDomainCombiner.java @@ -0,0 +1,123 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +package org.apache.harmony.javax.security.auth; + +import java.security.DomainCombiner; +import java.security.Principal; +import java.security.ProtectionDomain; +import java.util.Set; + +/** + * Merges permissions based on code source and code signers with permissions + * granted to the specified {@link Subject}. + */ +public class SubjectDomainCombiner implements DomainCombiner { + + // subject to be associated + private Subject subject; + + // permission required to get a subject object + private static final AuthPermission _GET = new AuthPermission( + "getSubjectFromDomainCombiner"); //$NON-NLS-1$ + + /** + * Creates a domain combiner for the entity provided in {@code subject}. + * + * @param subject + * the entity to which this domain combiner is associated. + */ + public SubjectDomainCombiner(Subject subject) { + super(); + if (subject == null) { + throw new NullPointerException(); + } + this.subject = subject; + } + + /** + * Returns the entity to which this domain combiner is associated. + * + * @return the entity to which this domain combiner is associated. + */ + public Subject getSubject() { + SecurityManager sm = System.getSecurityManager(); + if (sm != null) { + sm.checkPermission(_GET); + } + + return subject; + } + + /** + * Merges the {@code ProtectionDomain} with the {@code Principal}s + * associated with the subject of this {@code SubjectDomainCombiner}. + * + * @param currentDomains + * the {@code ProtectionDomain}s associated with the context of + * the current thread. The domains must be sorted according to + * the execution order, the most recent residing at the + * beginning. + * @param assignedDomains + * the {@code ProtectionDomain}s from the parent thread based on + * code source and signers. + * @return a single {@code ProtectionDomain} array computed from the two + * provided arrays, or {@code null}. + * @see ProtectionDomain + */ + public ProtectionDomain[] combine(ProtectionDomain[] currentDomains, + ProtectionDomain[] assignedDomains) { + // get array length for combining protection domains + int len = 0; + if (currentDomains != null) { + len += currentDomains.length; + } + if (assignedDomains != null) { + len += assignedDomains.length; + } + if (len == 0) { + return null; + } + + ProtectionDomain[] pd = new ProtectionDomain[len]; + + // for each current domain substitute set of principal with subject's + int cur = 0; + if (currentDomains != null) { + + Set<Principal> s = subject.getPrincipals(); + Principal[] p = s.toArray(new Principal[s.size()]); + + for (cur = 0; cur < currentDomains.length; cur++) { + if (currentDomains[cur] != null) { + ProtectionDomain newPD; + newPD = new ProtectionDomain(currentDomains[cur].getCodeSource(), + currentDomains[cur].getPermissions(), currentDomains[cur] + .getClassLoader(), p); + pd[cur] = newPD; + } + } + } + + // copy assigned domains + if (assignedDomains != null) { + System.arraycopy(assignedDomains, 0, pd, cur, assignedDomains.length); + } + + return pd; + } +} diff --git a/src/org/apache/harmony/javax/security/auth/callback/Callback.java b/src/org/apache/harmony/javax/security/auth/callback/Callback.java new file mode 100644 index 0000000..8fd745c --- /dev/null +++ b/src/org/apache/harmony/javax/security/auth/callback/Callback.java @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +package org.apache.harmony.javax.security.auth.callback; + +/** + * Defines an empty base interface for all {@code Callback}s used during + * authentication. + */ +public interface Callback { +}
\ No newline at end of file diff --git a/src/org/apache/harmony/javax/security/auth/callback/CallbackHandler.java b/src/org/apache/harmony/javax/security/auth/callback/CallbackHandler.java new file mode 100644 index 0000000..d09fafa --- /dev/null +++ b/src/org/apache/harmony/javax/security/auth/callback/CallbackHandler.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +package org.apache.harmony.javax.security.auth.callback; + +import java.io.IOException; + +/** + * Needs to be implemented by classes that want to handle authentication + * {@link Callback}s. A single method {@link #handle(Callback[])} must be + * provided that checks the type of the incoming {@code Callback}s and reacts + * accordingly. {@code CallbackHandler}s can be installed per application. It is + * also possible to configure a system-default {@code CallbackHandler} by + * setting the {@code auth.login.defaultCallbackHandler} property in the + * standard {@code security.properties} file. + */ +public interface CallbackHandler { + + /** + * Handles the actual {@link Callback}. A {@code CallbackHandler} needs to + * implement this method. In the method, it is free to select which {@code + * Callback}s it actually wants to handle and in which way. For example, a + * console-based {@code CallbackHandler} might choose to sequentially ask + * the user for login and password, if it implements these {@code Callback} + * s, whereas a GUI-based one might open a single dialog window for both + * values. If a {@code CallbackHandler} is not able to handle a specific + * {@code Callback}, it needs to throw an + * {@link UnsupportedCallbackException}. + * + * @param callbacks + * the array of {@code Callback}s that need handling + * @throws IOException + * if an I/O related error occurs + * @throws UnsupportedCallbackException + * if the {@code CallbackHandler} is not able to handle a + * specific {@code Callback} + */ + void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException; + +} diff --git a/src/org/apache/harmony/javax/security/auth/callback/ChoiceCallback.java b/src/org/apache/harmony/javax/security/auth/callback/ChoiceCallback.java new file mode 100644 index 0000000..1e53fb6 --- /dev/null +++ b/src/org/apache/harmony/javax/security/auth/callback/ChoiceCallback.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +package org.apache.harmony.javax.security.auth.callback; + +import java.io.Serializable; + + + +public class ChoiceCallback implements Callback, Serializable { + + private static final long serialVersionUID = -3975664071579892167L; + + private int defaultChoice; + + private String prompt; + + private boolean multipleSelectionsAllowed; + + private String[] choices; + + private int[] selections; + + private void setChoices(String[] choices) { + if (choices == null || choices.length == 0) { + throw new IllegalArgumentException("auth.1C"); //$NON-NLS-1$ + } + for (int i = 0; i < choices.length; i++) { + if (choices[i] == null || choices[i].length() == 0) { + throw new IllegalArgumentException("auth.1C"); //$NON-NLS-1$ + } + } + //FIXME: System.arraycopy(choices, 0 , new String[choices.length], 0, choices.length); + this.choices = choices; + + } + + private void setPrompt(String prompt) { + if (prompt == null || prompt.length() == 0) { + throw new IllegalArgumentException("auth.14"); //$NON-NLS-1$ + } + this.prompt = prompt; + } + + private void setDefaultChoice(int defaultChoice) { + if (0 > defaultChoice || defaultChoice >= choices.length) { + throw new IllegalArgumentException("auth.1D"); //$NON-NLS-1$ + } + this.defaultChoice = defaultChoice; + } + + public ChoiceCallback(String prompt, String[] choices, int defaultChoice, + boolean multipleSelectionsAllowed) { + super(); + setPrompt(prompt); + setChoices(choices); + setDefaultChoice(defaultChoice); + this.multipleSelectionsAllowed = multipleSelectionsAllowed; + } + + public boolean allowMultipleSelections() { + return multipleSelectionsAllowed; + } + + public String[] getChoices() { + return choices; + } + + public int getDefaultChoice() { + return defaultChoice; + } + + public String getPrompt() { + return prompt; + } + + public int[] getSelectedIndexes() { + return selections; + } + + public void setSelectedIndex(int selection) { + this.selections = new int[1]; + this.selections[0] = selection; + } + + public void setSelectedIndexes(int[] selections) { + if (!multipleSelectionsAllowed) { + throw new UnsupportedOperationException(); + } + this.selections = selections; + //FIXME: + // this.selections = new int[selections.length] + //System.arraycopy(selections, 0, this.selections, 0, this.selections.length); + } +} diff --git a/src/org/apache/harmony/javax/security/auth/callback/ConfirmationCallback.java b/src/org/apache/harmony/javax/security/auth/callback/ConfirmationCallback.java new file mode 100644 index 0000000..a1893f3 --- /dev/null +++ b/src/org/apache/harmony/javax/security/auth/callback/ConfirmationCallback.java @@ -0,0 +1,234 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +package org.apache.harmony.javax.security.auth.callback; + +import java.io.Serializable; + + + +public class ConfirmationCallback implements Callback, Serializable { + + private static final long serialVersionUID = -9095656433782481624L; + + public static final int YES = 0; // default options + + public static final int NO = 1; + + public static final int CANCEL = 2; + + public static final int OK = 3; + + public static final int YES_NO_OPTION = 0; // options type + + public static final int YES_NO_CANCEL_OPTION = 1; + + public static final int OK_CANCEL_OPTION = 2; + + public static final int UNSPECIFIED_OPTION = -1; + + public static final int INFORMATION = 0; // messages type + + public static final int WARNING = 1; + + public static final int ERROR = 2; + + private String prompt; + + private int messageType; + + private int optionType = UNSPECIFIED_OPTION; + + private int defaultOption; + + private String[] options; + + private int selection; + + public ConfirmationCallback(int messageType, int optionType, int defaultOption) { + super(); + if (messageType > ERROR || messageType < INFORMATION) { + throw new IllegalArgumentException("auth.16"); //$NON-NLS-1$ + } + + switch (optionType) { + case YES_NO_OPTION: + if (defaultOption != YES && defaultOption != NO) { + throw new IllegalArgumentException("auth.17"); //$NON-NLS-1$ + } + break; + case YES_NO_CANCEL_OPTION: + if (defaultOption != YES && defaultOption != NO && defaultOption != CANCEL) { + throw new IllegalArgumentException("auth.17"); //$NON-NLS-1$ + } + break; + case OK_CANCEL_OPTION: + if (defaultOption != OK && defaultOption != CANCEL) { + throw new IllegalArgumentException("auth.17"); //$NON-NLS-1$ + } + break; + default: + throw new IllegalArgumentException("auth.18"); //$NON-NLS-1$ + } + this.messageType = messageType; + this.optionType = optionType; + this.defaultOption = defaultOption; + } + + public ConfirmationCallback(int messageType, String[] options, int defaultOption) { + super(); + if (messageType > ERROR || messageType < INFORMATION) { + throw new IllegalArgumentException("auth.16"); //$NON-NLS-1$ + } + + if (options == null || options.length == 0) { + throw new IllegalArgumentException("auth.1A"); //$NON-NLS-1$ + } + for (int i = 0; i < options.length; i++) { + if (options[i] == null || options[i].length() == 0) { + throw new IllegalArgumentException("auth.1A"); //$NON-NLS-1$ + } + } + if (0 > defaultOption || defaultOption >= options.length) { + throw new IllegalArgumentException("auth.17"); //$NON-NLS-1$ + } + // FIXME:System.arraycopy(options, 0 , new String[this.options.length], + // 0, this.options.length); + this.options = options; + this.defaultOption = defaultOption; + this.messageType = messageType; + } + + public ConfirmationCallback(String prompt, int messageType, int optionType, + int defaultOption) { + super(); + if (prompt == null || prompt.length() == 0) { + throw new IllegalArgumentException("auth.14"); //$NON-NLS-1$ + } + + if (messageType > ERROR || messageType < INFORMATION) { + throw new IllegalArgumentException("auth.16"); //$NON-NLS-1$ + } + + switch (optionType) { + case YES_NO_OPTION: + if (defaultOption != YES && defaultOption != NO) { + throw new IllegalArgumentException("auth.17"); //$NON-NLS-1$ + } + break; + case YES_NO_CANCEL_OPTION: + if (defaultOption != YES && defaultOption != NO && defaultOption != CANCEL) { + throw new IllegalArgumentException("auth.17"); //$NON-NLS-1$ + } + break; + case OK_CANCEL_OPTION: + if (defaultOption != OK && defaultOption != CANCEL) { + throw new IllegalArgumentException("auth.17"); //$NON-NLS-1$ + } + break; + default: + throw new IllegalArgumentException("auth.18"); //$NON-NLS-1$ + } + this.prompt = prompt; + this.messageType = messageType; + this.optionType = optionType; + this.defaultOption = defaultOption; + } + + public ConfirmationCallback(String prompt, int messageType, String[] options, + int defaultOption) { + super(); + if (prompt == null || prompt.length() == 0) { + throw new IllegalArgumentException("auth.14"); //$NON-NLS-1$ + } + + if (messageType > ERROR || messageType < INFORMATION) { + throw new IllegalArgumentException("auth.16"); //$NON-NLS-1$ + } + + if (options == null || options.length == 0) { + throw new IllegalArgumentException("auth.1A"); //$NON-NLS-1$ + } + for (int i = 0; i < options.length; i++) { + if (options[i] == null || options[i].length() == 0) { + throw new IllegalArgumentException("auth.1A"); //$NON-NLS-1$ + } + } + if (0 > defaultOption || defaultOption >= options.length) { + throw new IllegalArgumentException("auth.17"); //$NON-NLS-1$ + } + // FIXME:System.arraycopy(options, 0 , new String[this.options.length], + // 0, this.options.length); + this.options = options; + this.defaultOption = defaultOption; + this.messageType = messageType; + this.prompt = prompt; + } + + public String getPrompt() { + return prompt; + } + + public int getMessageType() { + return messageType; + } + + public int getDefaultOption() { + return defaultOption; + } + + public String[] getOptions() { + return options; + } + + public int getOptionType() { + return optionType; + } + + public int getSelectedIndex() { + return selection; + } + + public void setSelectedIndex(int selection) { + if (options != null) { + if (0 <= selection && selection <= options.length) { + this.selection = selection; + } else { + throw new ArrayIndexOutOfBoundsException("auth.1B"); //$NON-NLS-1$ + } + } else { + switch (optionType) { + case YES_NO_OPTION: + if (selection != YES && selection != NO) { + throw new IllegalArgumentException("auth.19"); //$NON-NLS-1$ + } + break; + case YES_NO_CANCEL_OPTION: + if (selection != YES && selection != NO && selection != CANCEL) { + throw new IllegalArgumentException("auth.19"); //$NON-NLS-1$ + } + break; + case OK_CANCEL_OPTION: + if (selection != OK && selection != CANCEL) { + throw new IllegalArgumentException("auth.19"); //$NON-NLS-1$ + } + break; + } + this.selection = selection; + } + } +} diff --git a/src/org/apache/harmony/javax/security/auth/callback/LanguageCallback.java b/src/org/apache/harmony/javax/security/auth/callback/LanguageCallback.java new file mode 100644 index 0000000..729bb49 --- /dev/null +++ b/src/org/apache/harmony/javax/security/auth/callback/LanguageCallback.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +package org.apache.harmony.javax.security.auth.callback; + +import java.io.Serializable; +import java.util.Locale; + +public class LanguageCallback implements Callback, Serializable { + + private static final long serialVersionUID = 2019050433478903213L; + + private Locale locale; + + public LanguageCallback() { + super(); + } + + public Locale getLocale() { + return locale; + } + + public void setLocale(Locale locale) { + this.locale = locale; + } + +} diff --git a/src/org/apache/harmony/javax/security/auth/callback/NameCallback.java b/src/org/apache/harmony/javax/security/auth/callback/NameCallback.java new file mode 100644 index 0000000..97264ef --- /dev/null +++ b/src/org/apache/harmony/javax/security/auth/callback/NameCallback.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +package org.apache.harmony.javax.security.auth.callback; + +import java.io.Serializable; + + + +public class NameCallback implements Callback, Serializable { + + private static final long serialVersionUID = 3770938795909392253L; + + private String prompt; + + private String defaultName; + + private String inputName; + + private void setPrompt(String prompt) { + if (prompt == null || prompt.length() == 0) { + throw new IllegalArgumentException("auth.14"); //$NON-NLS-1$ + } + this.prompt = prompt; + } + + private void setDefaultName(String defaultName) { + if (defaultName == null || defaultName.length() == 0) { + throw new IllegalArgumentException("auth.1E"); //$NON-NLS-1$ + } + this.defaultName = defaultName; + } + + public NameCallback(String prompt) { + super(); + setPrompt(prompt); + } + + public NameCallback(String prompt, String defaultName) { + super(); + setPrompt(prompt); + setDefaultName(defaultName); + } + + public String getPrompt() { + return prompt; + } + + public String getDefaultName() { + return defaultName; + } + + public void setName(String name) { + this.inputName = name; + } + + public String getName() { + return inputName; + } +} diff --git a/src/org/apache/harmony/javax/security/auth/callback/PasswordCallback.java b/src/org/apache/harmony/javax/security/auth/callback/PasswordCallback.java new file mode 100644 index 0000000..bd142fc --- /dev/null +++ b/src/org/apache/harmony/javax/security/auth/callback/PasswordCallback.java @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +package org.apache.harmony.javax.security.auth.callback; + +import java.io.Serializable; +import java.util.Arrays; + + + +/** + * Is used in conjunction with a {@link CallbackHandler} to retrieve a password + * when needed. + */ +public class PasswordCallback implements Callback, Serializable { + + private static final long serialVersionUID = 2267422647454909926L; + + private String prompt; + + boolean echoOn; + + private char[] inputPassword; + + private void setPrompt(String prompt) throws IllegalArgumentException { + if (prompt == null || prompt.length() == 0) { + throw new IllegalArgumentException("auth.14"); //$NON-NLS-1$ + } + this.prompt = prompt; + } + + /** + * Creates a new {@code PasswordCallback} instance. + * + * @param prompt + * the message that should be displayed to the user + * @param echoOn + * determines whether the user input should be echoed + */ + public PasswordCallback(String prompt, boolean echoOn) { + super(); + setPrompt(prompt); + this.echoOn = echoOn; + } + + /** + * Returns the prompt that was specified when creating this {@code + * PasswordCallback} + * + * @return the prompt + */ + public String getPrompt() { + return prompt; + } + + /** + * Queries whether this {@code PasswordCallback} expects user input to be + * echoed, which is specified during the creation of the object. + * + * @return {@code true} if (and only if) user input should be echoed + */ + public boolean isEchoOn() { + return echoOn; + } + + /** + * Sets the password. The {@link CallbackHandler} that performs the actual + * provisioning or input of the password needs to call this method to hand + * back the password to the security service that requested it. + * + * @param password + * the password. A copy of this is stored, so subsequent changes + * to the input array do not affect the {@code PasswordCallback}. + */ + public void setPassword(char[] password) { + if (password == null) { + this.inputPassword = password; + } else { + inputPassword = new char[password.length]; + System.arraycopy(password, 0, inputPassword, 0, inputPassword.length); + } + } + + /** + * Returns the password. The security service that needs the password + * usually calls this method once the {@link CallbackHandler} has finished + * its work. + * + * @return the password. A copy of the internal password is created and + * returned, so subsequent changes to the internal password do not + * affect the result. + */ + public char[] getPassword() { + if (inputPassword != null) { + char[] tmp = new char[inputPassword.length]; + System.arraycopy(inputPassword, 0, tmp, 0, tmp.length); + return tmp; + } + return null; + } + + /** + * Clears the password stored in this {@code PasswordCallback}. + */ + public void clearPassword() { + if (inputPassword != null) { + Arrays.fill(inputPassword, '\u0000'); + } + } +} diff --git a/src/org/apache/harmony/javax/security/auth/callback/TextInputCallback.java b/src/org/apache/harmony/javax/security/auth/callback/TextInputCallback.java new file mode 100644 index 0000000..c7de222 --- /dev/null +++ b/src/org/apache/harmony/javax/security/auth/callback/TextInputCallback.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +package org.apache.harmony.javax.security.auth.callback; + +import java.io.Serializable; + + + +public class TextInputCallback implements Callback, Serializable { + + private static final long serialVersionUID = -8064222478852811804L; + + private String defaultText; + + private String prompt; + + private String inputText; + + private void setPrompt(String prompt) { + if (prompt == null || prompt.length() == 0) { + throw new IllegalArgumentException("auth.14"); //$NON-NLS-1$ + } + this.prompt = prompt; + } + + private void setDefaultText(String defaultText) { + if (defaultText == null || defaultText.length() == 0) { + throw new IllegalArgumentException("auth.15"); //$NON-NLS-1$ + } + this.defaultText = defaultText; + } + + public TextInputCallback(String prompt) { + super(); + setPrompt(prompt); + } + + public TextInputCallback(String prompt, String defaultText) { + super(); + setPrompt(prompt); + setDefaultText(defaultText); + } + + public String getDefaultText() { + return defaultText; + } + + public String getPrompt() { + return prompt; + } + + public String getText() { + return inputText; + } + + public void setText(String text) { + this.inputText = text; + } +} diff --git a/src/org/apache/harmony/javax/security/auth/callback/TextOutputCallback.java b/src/org/apache/harmony/javax/security/auth/callback/TextOutputCallback.java new file mode 100644 index 0000000..23a72fa --- /dev/null +++ b/src/org/apache/harmony/javax/security/auth/callback/TextOutputCallback.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +package org.apache.harmony.javax.security.auth.callback; + +import java.io.Serializable; + + + +public class TextOutputCallback implements Callback, Serializable { + + private static final long serialVersionUID = 1689502495511663102L; + + public static final int INFORMATION = 0; + + public static final int WARNING = 1; + + public static final int ERROR = 2; + + private String message; + + private int messageType; + + public TextOutputCallback(int messageType, String message) { + if (messageType > ERROR || messageType < INFORMATION) { + throw new IllegalArgumentException("auth.16"); //$NON-NLS-1$ + } + if (message == null || message.length() == 0) { + throw new IllegalArgumentException("auth.1F"); //$NON-NLS-1$ + } + this.messageType = messageType; + this.message = message; + } + + public String getMessage() { + return message; + } + + public int getMessageType() { + return messageType; + } +} diff --git a/src/org/apache/harmony/javax/security/auth/callback/UnsupportedCallbackException.java b/src/org/apache/harmony/javax/security/auth/callback/UnsupportedCallbackException.java new file mode 100644 index 0000000..19f6e40 --- /dev/null +++ b/src/org/apache/harmony/javax/security/auth/callback/UnsupportedCallbackException.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +package org.apache.harmony.javax.security.auth.callback; + +/** + * Thrown when a {@link CallbackHandler} does not support a particular {@link + * Callback}. + */ +public class UnsupportedCallbackException extends Exception { + + private static final long serialVersionUID = -6873556327655666839L; + + private Callback callback; + + /** + * Creates a new exception instance and initializes it with just the + * unsupported {@code Callback}, but no error message. + * + * @param callback + * the {@code Callback} + */ + public UnsupportedCallbackException(Callback callback) { + super(); + this.callback = callback; + } + + /** + * Creates a new exception instance and initializes it with both the + * unsupported {@code Callback} and an error message. + * + * @param callback + * the {@code Callback} + * @param message + * the error message + */ + public UnsupportedCallbackException(Callback callback, String message) { + super(message); + this.callback = callback; + } + + /** + * Returns the unsupported {@code Callback} that triggered this exception. + * + * @return the {@code Callback} + */ + public Callback getCallback() { + return callback; + } +} diff --git a/src/org/apache/harmony/javax/security/auth/login/AccountException.java b/src/org/apache/harmony/javax/security/auth/login/AccountException.java new file mode 100644 index 0000000..c86e801 --- /dev/null +++ b/src/org/apache/harmony/javax/security/auth/login/AccountException.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +package org.apache.harmony.javax.security.auth.login; + +public class AccountException extends LoginException { + + private static final long serialVersionUID = -2112878680072211787L; + + public AccountException() { + super(); + } + + public AccountException(String message) { + super(message); + } +} |