ByteArrayBuilder class to build byte sequences; BasicScheme and DigestScheme optimized to generate less intermediate garbage

git-svn-id: https://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk@1695156 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Oleg Kalnichevski 2015-08-10 19:44:09 +00:00
parent e52e269dc8
commit d09ae9707d
5 changed files with 495 additions and 47 deletions

View File

@ -0,0 +1,212 @@
/*
* ====================================================================
* 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.
* ====================================================================
*
* 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 org.apache.http.auth.util;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;
import org.apache.http.Consts;
import org.apache.http.annotation.Immutable;
/**
* Builder class for sequences of bytes.
*
* @since 5.0
*/
@Immutable
public class ByteArrayBuilder {
private CharsetEncoder charsetEncoder;
private ByteBuffer buffer;
public ByteArrayBuilder() {
}
public ByteArrayBuilder(final int initialCapacity) {
this.buffer = ByteBuffer.allocate(initialCapacity);
}
public int capacity() {
return this.buffer != null ? this.buffer.capacity() : 0;
}
static ByteBuffer ensureFreeCapacity(final ByteBuffer buffer, final int capacity) {
if (buffer == null) {
return ByteBuffer.allocate(capacity);
}
if (buffer.remaining() < capacity) {
final ByteBuffer newBuffer = ByteBuffer.allocate(buffer.position() + capacity);
buffer.flip();
newBuffer.put(buffer);
return newBuffer;
} else {
return buffer;
}
}
static ByteBuffer encode(
final ByteBuffer buffer, final CharBuffer in, final CharsetEncoder encoder) throws CharacterCodingException {
final int capacity = (int) (in.remaining() * encoder.averageBytesPerChar());
ByteBuffer out = ensureFreeCapacity(buffer, capacity);
for (;;) {
CoderResult result = in.hasRemaining() ? encoder.encode(in, out, true) : CoderResult.UNDERFLOW;
if (result.isError()) {
result.throwException();
}
if (result.isUnderflow()) {
result = encoder.flush(out);
}
if (result.isUnderflow()) {
break;
}
if (result.isOverflow()) {
out = ensureFreeCapacity(out, capacity);
}
}
return out;
}
public void ensureFreeCapacity(final int freeCapacity) {
this.buffer = ensureFreeCapacity(this.buffer, freeCapacity);
}
private void doAppend(final CharBuffer charBuffer) {
if (this.charsetEncoder == null) {
this.charsetEncoder = Consts.ASCII.newEncoder()
.onMalformedInput(CodingErrorAction.IGNORE)
.onUnmappableCharacter(CodingErrorAction.REPLACE);
}
this.charsetEncoder.reset();
try {
this.buffer = encode(this.buffer, charBuffer, this.charsetEncoder);
} catch (CharacterCodingException ex) {
// Should never happen
throw new IllegalStateException("Unexpected character coding error", ex);
}
}
public ByteArrayBuilder charset(final Charset charset) {
if (charset == null) {
this.charsetEncoder = null;
} else {
this.charsetEncoder = charset.newEncoder()
.onMalformedInput(CodingErrorAction.IGNORE)
.onUnmappableCharacter(CodingErrorAction.REPLACE);
}
return this;
}
public ByteArrayBuilder append(final byte[] b, final int off, final int len) {
if (b == null) {
return this;
}
if ((off < 0) || (off > b.length) || (len < 0) ||
((off + len) < 0) || ((off + len) > b.length)) {
throw new IndexOutOfBoundsException("off: " + off + " len: " + len + " b.length: " + b.length);
}
ensureFreeCapacity(len);
this.buffer.put(b, off, len);
return this;
}
public ByteArrayBuilder append(final byte[] b) {
if (b == null) {
return this;
}
return append(b, 0, b.length);
}
public ByteArrayBuilder append(final CharBuffer charBuffer) {
if (charBuffer == null) {
return this;
}
doAppend(charBuffer);
return this;
}
public ByteArrayBuilder append(final char[] b, final int off, final int len) {
if (b == null) {
return this;
}
if ((off < 0) || (off > b.length) || (len < 0) ||
((off + len) < 0) || ((off + len) > b.length)) {
throw new IndexOutOfBoundsException("off: " + off + " len: " + len + " b.length: " + b.length);
}
return append(CharBuffer.wrap(b, off, len));
}
public ByteArrayBuilder append(final char[] b) {
if (b == null) {
return this;
}
return append(b, 0, b.length);
}
public ByteArrayBuilder append(final String s) {
if (s == null) {
return this;
}
return append(CharBuffer.wrap(s));
}
public ByteBuffer toByteBuffer() {
return this.buffer != null ? this.buffer.duplicate() : ByteBuffer.allocate(0);
}
public byte[] toByteArray() {
if (this.buffer == null) {
return new byte[] {};
} else {
this.buffer.flip();
final byte[] b = new byte[this.buffer.remaining()];
this.buffer.get(b);
this.buffer.clear();
return b;
}
}
public void reset() {
if (this.charsetEncoder != null) {
this.charsetEncoder.reset();
}
if (this.buffer != null) {
this.buffer.clear();
}
}
@Override
public String toString() {
return this.buffer != null ? this.buffer.toString() : "null";
}
}

View File

@ -51,10 +51,10 @@
import org.apache.http.auth.Credentials;
import org.apache.http.auth.CredentialsProvider;
import org.apache.http.auth.MalformedChallengeException;
import org.apache.http.auth.util.ByteArrayBuilder;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.Args;
import org.apache.http.util.CharsetUtils;
import org.apache.http.util.EncodingUtils;
/**
* Basic authentication scheme as defined in RFC 2617.
@ -68,7 +68,10 @@ public class BasicScheme implements AuthScheme, Serializable {
private final Map<String, String> paramMap;
private transient Charset charset;
private transient ByteArrayBuilder buffer;
private transient Base64 base64codec;
private boolean complete;
private String username;
private String password;
@ -160,12 +163,17 @@ public String generateAuthResponse(
final HttpHost host,
final HttpRequest request,
final HttpContext context) throws AuthenticationException {
final StringBuilder buffer = new StringBuilder();
buffer.append(this.username);
buffer.append(":");
buffer.append(this.password);
final Base64 base64codec = new Base64(0);
final byte[] encodedCreds = base64codec.encode(EncodingUtils.getBytes(buffer.toString(), charset.name()));
if (this.buffer == null) {
this.buffer = new ByteArrayBuilder(64).charset(this.charset);
} else {
this.buffer.reset();
}
this.buffer.append(this.username).append(":").append(this.password);
if (this.base64codec == null) {
this.base64codec = new Base64(0);
}
final byte[] encodedCreds = this.base64codec.encode(this.buffer.toByteArray());
this.buffer.reset();
return "Basic " + new String(encodedCreds, 0, encodedCreds.length, Consts.ASCII);
}

View File

@ -28,6 +28,7 @@
import java.io.IOException;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.Principal;
import java.security.SecureRandom;
@ -41,6 +42,7 @@
import java.util.Set;
import java.util.StringTokenizer;
import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpHost;
@ -54,12 +56,13 @@
import org.apache.http.auth.Credentials;
import org.apache.http.auth.CredentialsProvider;
import org.apache.http.auth.MalformedChallengeException;
import org.apache.http.auth.util.ByteArrayBuilder;
import org.apache.http.message.BasicHeaderValueFormatter;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.Args;
import org.apache.http.util.CharArrayBuffer;
import org.apache.http.util.EncodingUtils;
import org.apache.http.util.CharsetUtils;
/**
* Digest authentication scheme as defined in RFC 2617.
@ -84,7 +87,7 @@ public class DigestScheme implements AuthScheme, Serializable {
* Hexa values used when creating 32 character long digest in HTTP DigestScheme
* in case of authentication.
*
* @see #encode(byte[])
* @see #formatHex(byte[])
*/
private static final char[] HEXADECIMAL = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
@ -98,11 +101,13 @@ public class DigestScheme implements AuthScheme, Serializable {
private final Map<String, String> paramMap;
private boolean complete;
private transient ByteArrayBuilder buffer;
private String lastNonce;
private long nounceCount;
private String cnonce;
private String a1;
private String a2;
private byte[] a1;
private byte[] a2;
private String username;
private String password;
@ -251,9 +256,10 @@ private String createDigestResponse(final HttpRequest request) throws Authentica
throw new AuthenticationException("None of the qop methods is supported: " + qoplist);
}
String charset = this.paramMap.get("charset");
final String charsetName = this.paramMap.get("charset");
Charset charset = charsetName != null ? CharsetUtils.lookup(charsetName) : null;
if (charset == null) {
charset = "ISO-8859-1";
charset = Consts.ISO_8859_1;
}
String digAlg = algorithm;
@ -275,16 +281,24 @@ private String createDigestResponse(final HttpRequest request) throws Authentica
cnonce = null;
lastNonce = nonce;
}
final StringBuilder sb = new StringBuilder(256);
final StringBuilder sb = new StringBuilder(8);
final Formatter formatter = new Formatter(sb, Locale.US);
formatter.format("%08x", Long.valueOf(nounceCount));
formatter.format("%08x", nounceCount);
formatter.close();
final String nc = sb.toString();
if (cnonce == null) {
cnonce = createCnonce();
cnonce = formatHex(createCnonce());
}
if (buffer == null) {
buffer = new ByteArrayBuilder();
} else {
buffer.reset();
}
buffer.charset(charset);
a1 = null;
a2 = null;
// 3.2.2.2: Calculating digest
@ -294,24 +308,23 @@ private String createDigestResponse(final HttpRequest request) throws Authentica
// ":" unq(cnonce-value)
// calculated one per session
sb.setLength(0);
sb.append(username).append(':').append(realm).append(':').append(password);
final String checksum = encode(digester.digest(EncodingUtils.getBytes(sb.toString(), charset)));
sb.setLength(0);
sb.append(checksum).append(':').append(nonce).append(':').append(cnonce);
a1 = sb.toString();
buffer.append(username).append(":").append(realm).append(":").append(password);
final String checksum = formatHex(digester.digest(this.buffer.toByteArray()));
buffer.reset();
buffer.append(checksum).append(":").append(nonce).append(":").append(cnonce);
a1 = buffer.toByteArray();
} else {
// unq(username-value) ":" unq(realm-value) ":" passwd
sb.setLength(0);
sb.append(username).append(':').append(realm).append(':').append(password);
a1 = sb.toString();
buffer.append(username).append(":").append(realm).append(":").append(password);
a1 = buffer.toByteArray();
}
final String hasha1 = encode(digester.digest(EncodingUtils.getBytes(a1, charset)));
final String hasha1 = formatHex(digester.digest(a1));
buffer.reset();
if (qop == QOP_AUTH) {
// Method ":" digest-uri-value
a2 = method + ':' + uri;
a2 = buffer.append(method).append(":").append(uri).toByteArray();
} else if (qop == QOP_AUTH_INT) {
// Method ":" digest-uri-value ":" H(entity-body)
HttpEntity entity = null;
@ -322,7 +335,7 @@ private String createDigestResponse(final HttpRequest request) throws Authentica
// If the entity is not repeatable, try falling back onto QOP_AUTH
if (qopset.contains("auth")) {
qop = QOP_AUTH;
a2 = method + ':' + uri;
a2 = buffer.append(method).append(":").append(uri).toByteArray();
} else {
throw new AuthenticationException("Qop auth-int cannot be used with " +
"a non-repeatable entity");
@ -337,30 +350,31 @@ private String createDigestResponse(final HttpRequest request) throws Authentica
} catch (final IOException ex) {
throw new AuthenticationException("I/O error reading entity content", ex);
}
a2 = method + ':' + uri + ':' + encode(entityDigester.getDigest());
a2 = buffer.append(method).append(":").append(uri)
.append(":").append(formatHex(entityDigester.getDigest())).toByteArray();
}
} else {
a2 = method + ':' + uri;
a2 = buffer.append(method).append(":").append(uri).toByteArray();
}
final String hasha2 = encode(digester.digest(EncodingUtils.getBytes(a2, charset)));
final String hasha2 = formatHex(digester.digest(a2));
buffer.reset();
// 3.2.2.1
final String digestValue;
final byte[] digestInput;
if (qop == QOP_MISSING) {
sb.setLength(0);
sb.append(hasha1).append(':').append(nonce).append(':').append(hasha2);
digestValue = sb.toString();
buffer.append(hasha1).append(":").append(nonce).append(":").append(hasha2);
digestInput = buffer.toByteArray();
} else {
sb.setLength(0);
sb.append(hasha1).append(':').append(nonce).append(':').append(nc).append(':')
.append(cnonce).append(':').append(qop == QOP_AUTH_INT ? "auth-int" : "auth")
.append(':').append(hasha2);
digestValue = sb.toString();
buffer.append(hasha1).append(":").append(nonce).append(":").append(nc).append(":")
.append(cnonce).append(":").append(qop == QOP_AUTH_INT ? "auth-int" : "auth")
.append(":").append(hasha2);
digestInput = buffer.toByteArray();
}
buffer.reset();
final String digest = encode(digester.digest(EncodingUtils.getAsciiBytes(digestValue)));
final String digest = formatHex(digester.digest(digestInput));
final CharArrayBuffer buffer = new CharArrayBuffer(128);
buffer.append("Digest ");
@ -401,11 +415,11 @@ String getCnonce() {
}
String getA1() {
return a1;
return a1 != null ? new String(a1, Consts.ASCII) : null;
}
String getA2() {
return a2;
return a2 != null ? new String(a2, Consts.ASCII) : null;
}
/**
@ -415,7 +429,7 @@ String getA2() {
* @param binaryData array containing the digest
* @return encoded MD5, or <CODE>null</CODE> if encoding failed
*/
static String encode(final byte[] binaryData) {
static String formatHex(final byte[] binaryData) {
final int n = binaryData.length;
final char[] buffer = new char[n * 2];
for (int i = 0; i < n; i++) {
@ -433,11 +447,11 @@ static String encode(final byte[] binaryData) {
*
* @return The cnonce value as String.
*/
public static String createCnonce() {
static byte[] createCnonce() {
final SecureRandom rnd = new SecureRandom();
final byte[] tmp = new byte[8];
rnd.nextBytes(tmp);
return encode(tmp);
return tmp;
}
@Override

View File

@ -0,0 +1,214 @@
/*
* ====================================================================
* 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.
* ====================================================================
*
* 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 org.apache.http.auth.util;
import java.nio.ByteBuffer;
import org.apache.http.Consts;
import org.junit.Assert;
import org.junit.Test;
/**
* {@link ByteArrayBuilder} test cases.
*/
public class TestByteArrayBuilder {
@Test
public void testEmptyBuffer() throws Exception {
final ByteArrayBuilder buffer = new ByteArrayBuilder();
final ByteBuffer byteBuffer = buffer.toByteBuffer();
Assert.assertNotNull(byteBuffer);
Assert.assertEquals(0, byteBuffer.capacity());
final byte[] bytes = buffer.toByteArray();
Assert.assertNotNull(bytes);
Assert.assertEquals(0, bytes.length);
}
@Test
public void testAppendBytes() throws Exception {
final ByteArrayBuilder buffer = new ByteArrayBuilder();
buffer.append(new byte[]{1, 2, 3, 4, 5});
buffer.append(new byte[]{3, 4, 5, 6, 7, 8, 9, 10, 11}, 3, 5);
buffer.append((byte[]) null);
final byte[] bytes = buffer.toByteArray();
Assert.assertNotNull(bytes);
Assert.assertArrayEquals(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, bytes);
}
@Test
public void testInvalidAppendBytes() throws Exception {
final ByteArrayBuilder buffer = new ByteArrayBuilder();
buffer.append((byte[])null, 0, 0);
final byte[] tmp = new byte[] { 1, 2, 3, 4};
try {
buffer.append(tmp, -1, 0);
Assert.fail("IndexOutOfBoundsException should have been thrown");
} catch (final IndexOutOfBoundsException ex) {
// expected
}
try {
buffer.append(tmp, 0, -1);
Assert.fail("IndexOutOfBoundsException should have been thrown");
} catch (final IndexOutOfBoundsException ex) {
// expected
}
try {
buffer.append(tmp, 0, 8);
Assert.fail("IndexOutOfBoundsException should have been thrown");
} catch (final IndexOutOfBoundsException ex) {
// expected
}
try {
buffer.append(tmp, 10, Integer.MAX_VALUE);
Assert.fail("IndexOutOfBoundsException should have been thrown");
} catch (final IndexOutOfBoundsException ex) {
// expected
}
try {
buffer.append(tmp, 2, 4);
Assert.fail("IndexOutOfBoundsException should have been thrown");
} catch (final IndexOutOfBoundsException ex) {
// expected
}
}
@Test
public void testEnsureCapacity() throws Exception {
final ByteArrayBuilder buffer = new ByteArrayBuilder();
buffer.ensureFreeCapacity(10);
Assert.assertEquals(10, buffer.capacity());
buffer.ensureFreeCapacity(5);
Assert.assertEquals(10, buffer.capacity());
buffer.append(new byte[]{1, 2, 3, 4, 5, 6, 7, 8});
buffer.ensureFreeCapacity(5);
Assert.assertEquals(13, buffer.capacity());
buffer.ensureFreeCapacity(15);
Assert.assertEquals(23, buffer.capacity());
}
@Test
public void testAppendText() throws Exception {
final ByteArrayBuilder buffer = new ByteArrayBuilder();
buffer.append(new char[]{'1', '2', '3', '4', '5'});
buffer.append(new char[]{'3', '4', '5', '6', '7', '8', '9', 'a', 'b'}, 3, 5);
buffer.append("bcd");
buffer.append("e");
buffer.append("f");
buffer.append((String) null);
buffer.append((char[]) null);
final byte[] bytes = buffer.toByteArray();
Assert.assertNotNull(bytes);
Assert.assertEquals("123456789abcdef", new String(bytes, Consts.ASCII));
}
@Test
public void testInvalidAppendChars() throws Exception {
final ByteArrayBuilder buffer = new ByteArrayBuilder();
buffer.append((char[])null, 0, 0);
final char[] tmp = new char[] { 1, 2, 3, 4};
try {
buffer.append(tmp, -1, 0);
Assert.fail("IndexOutOfBoundsException should have been thrown");
} catch (final IndexOutOfBoundsException ex) {
// expected
}
try {
buffer.append(tmp, 0, -1);
Assert.fail("IndexOutOfBoundsException should have been thrown");
} catch (final IndexOutOfBoundsException ex) {
// expected
}
try {
buffer.append(tmp, 0, 8);
Assert.fail("IndexOutOfBoundsException should have been thrown");
} catch (final IndexOutOfBoundsException ex) {
// expected
}
try {
buffer.append(tmp, 10, Integer.MAX_VALUE);
Assert.fail("IndexOutOfBoundsException should have been thrown");
} catch (final IndexOutOfBoundsException ex) {
// expected
}
try {
buffer.append(tmp, 2, 4);
Assert.fail("IndexOutOfBoundsException should have been thrown");
} catch (final IndexOutOfBoundsException ex) {
// expected
}
}
@Test
public void testReset() throws Exception {
final ByteArrayBuilder buffer = new ByteArrayBuilder();
buffer.append("abcd");
buffer.append("e");
buffer.append("f");
final byte[] bytes1 = buffer.toByteArray();
Assert.assertNotNull(bytes1);
Assert.assertEquals("abcdef", new String(bytes1, Consts.ASCII));
buffer.reset();
final byte[] bytes2 = buffer.toByteArray();
Assert.assertNotNull(bytes2);
Assert.assertEquals("", new String(bytes2, Consts.ASCII));
}
@Test
public void testNonAsciiCharset() throws Exception {
final int[] germanChars = { 0xE4, 0x2D, 0xF6, 0x2D, 0xFc };
final StringBuilder tmp = new StringBuilder();
for (final int germanChar : germanChars) {
tmp.append((char) germanChar);
}
final String umlauts = tmp.toString();
final ByteArrayBuilder buffer = new ByteArrayBuilder();
buffer.append(umlauts);
final byte[] bytes1 = buffer.toByteArray();
Assert.assertNotNull(bytes1);
Assert.assertEquals("?-?-?", new String(bytes1, Consts.ASCII));
buffer.reset();
buffer.charset(Consts.UTF_8);
buffer.append(umlauts);
final byte[] bytes2 = buffer.toByteArray();
Assert.assertNotNull(bytes2);
Assert.assertEquals(umlauts, new String(bytes2, Consts.UTF_8));
}
}

View File

@ -567,7 +567,7 @@ public void testHttpEntityDigest() throws Exception {
digester.write(new byte[] { 'a', 'b', 'c'});
Assert.assertNull(digester.getDigest());
digester.close();
Assert.assertEquals("acd2b59cd01c7737d8069015584c6cac", DigestScheme.encode(digester.getDigest()));
Assert.assertEquals("acd2b59cd01c7737d8069015584c6cac", DigestScheme.formatHex(digester.getDigest()));
try {
digester.write('a');
Assert.fail("IOException should have been thrown");