diff --git a/httpclient/src/main/java/org/apache/http/auth/AuthChallenge.java b/httpclient/src/main/java/org/apache/http/auth/AuthChallenge.java
new file mode 100644
index 000000000..c6bbb073c
--- /dev/null
+++ b/httpclient/src/main/java/org/apache/http/auth/AuthChallenge.java
@@ -0,0 +1,83 @@
+/*
+ * ====================================================================
+ * 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
+ * .
+ *
+ */
+package org.apache.http.auth;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.http.NameValuePair;
+import org.apache.http.annotation.Immutable;
+import org.apache.http.util.Args;
+
+/**
+ * This class represents an authentication challenge consisting of a auth scheme
+ * and either a single parameter or a list of name / value pairs.
+ *
+ * @since 5.0
+ */
+@Immutable
+public final class AuthChallenge {
+
+ private final String scheme;
+ private final String value;
+ private final List params;
+
+ public AuthChallenge(final String scheme, final String value, final List extends NameValuePair> params) {
+ super();
+ Args.notNull(scheme, "Auth scheme");
+ this.scheme = scheme;
+ this.value = value;
+ this.params = params != null ? Collections.unmodifiableList(new ArrayList<>(params)) : null;
+ }
+
+ public String getScheme() {
+ return scheme;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public List getParams() {
+ return params;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder buffer = new StringBuilder();
+ buffer.append(scheme).append(" ");
+ if (value != null) {
+ buffer.append(value);
+ } else if (params != null) {
+ buffer.append(params);
+ }
+ return buffer.toString();
+ }
+
+}
+
diff --git a/httpclient/src/main/java/org/apache/http/impl/auth/AuthChallengeParser.java b/httpclient/src/main/java/org/apache/http/impl/auth/AuthChallengeParser.java
new file mode 100644
index 000000000..e64fdb01f
--- /dev/null
+++ b/httpclient/src/main/java/org/apache/http/impl/auth/AuthChallengeParser.java
@@ -0,0 +1,122 @@
+/*
+ * ====================================================================
+ * 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
+ * .
+ *
+ */
+
+package org.apache.http.impl.auth;
+
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.List;
+
+import org.apache.http.NameValuePair;
+import org.apache.http.ParseException;
+import org.apache.http.auth.AuthChallenge;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.message.ParserCursor;
+import org.apache.http.message.TokenParser;
+import org.apache.http.util.CharArrayBuffer;
+
+public class AuthChallengeParser {
+
+ private final TokenParser tokenParser = TokenParser.INSTANCE;
+
+ private final static char BLANK = ' ';
+ private final static char COMMA_CHAR = ',';
+ private final static char EQUAL_CHAR = '=';
+
+ // IMPORTANT!
+ // These private static variables must be treated as immutable and never exposed outside this class
+ private static final BitSet TERMINATORS = TokenParser.INIT_BITSET(BLANK, EQUAL_CHAR, COMMA_CHAR);
+ private static final BitSet DELIMITER = TokenParser.INIT_BITSET(COMMA_CHAR);
+
+ NameValuePair parseTokenOrParameter(final CharArrayBuffer buffer, final ParserCursor cursor) throws ParseException {
+
+ tokenParser.skipWhiteSpace(buffer, cursor);
+ final String token = tokenParser.parseToken(buffer, cursor, TERMINATORS);
+ if (!cursor.atEnd()) {
+ if (buffer.charAt(cursor.getPos()) == BLANK) {
+ tokenParser.skipWhiteSpace(buffer, cursor);
+ }
+ if (buffer.charAt(cursor.getPos()) == EQUAL_CHAR) {
+ cursor.updatePos(cursor.getPos() + 1);
+ final String value = tokenParser.parseValue(buffer, cursor, DELIMITER);
+ return new BasicNameValuePair(token, value);
+ }
+ }
+ return new BasicNameValuePair(token, null);
+ }
+
+ public List parse(final CharArrayBuffer buffer, final ParserCursor cursor) throws ParseException {
+
+ final List list = new ArrayList<>();
+ String scheme = null;
+ final List params = new ArrayList<>();
+ while (!cursor.atEnd()) {
+ final NameValuePair tokenOrParameter = parseTokenOrParameter(buffer, cursor);
+ if (tokenOrParameter.getValue() == null && !cursor.atEnd() && buffer.charAt(cursor.getPos()) != COMMA_CHAR) {
+ if (scheme != null) {
+ if (params.isEmpty()) {
+ throw new ParseException("Malformed auth challenge");
+ }
+ list.add(createAuthChallenge(scheme, params));
+ params.clear();
+ }
+ scheme = tokenOrParameter.getName();
+ } else {
+ params.add(tokenOrParameter);
+ if (!cursor.atEnd() && buffer.charAt(cursor.getPos()) != COMMA_CHAR) {
+ scheme = null;
+ }
+ }
+ if (!cursor.atEnd() && buffer.charAt(cursor.getPos()) == COMMA_CHAR) {
+ cursor.updatePos(cursor.getPos() + 1);
+ }
+ }
+ list.add(createAuthChallenge(scheme, params));
+ return list;
+ }
+
+ private static AuthChallenge createAuthChallenge(final String scheme, final List params) {
+ if (scheme != null) {
+ if (params.size() == 1) {
+ final NameValuePair nvp = params.get(0);
+ if (nvp.getValue() == null) {
+ return new AuthChallenge(scheme, nvp.getName(), null);
+ }
+ }
+ return new AuthChallenge(scheme, null, params.size() > 0 ? params : null);
+ } else {
+ if (params.size() == 1) {
+ final NameValuePair nvp = params.get(0);
+ if (nvp.getValue() == null) {
+ return new AuthChallenge(nvp.getName(), null, null);
+ }
+ }
+ throw new ParseException("Malformed auth challenge");
+ }
+ }
+
+}
diff --git a/httpclient/src/test/java/org/apache/http/auth/TestAuthChallenge.java b/httpclient/src/test/java/org/apache/http/auth/TestAuthChallenge.java
new file mode 100644
index 000000000..a2700e61f
--- /dev/null
+++ b/httpclient/src/test/java/org/apache/http/auth/TestAuthChallenge.java
@@ -0,0 +1,57 @@
+/*
+ * ====================================================================
+ * 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
+ * .
+ *
+ */
+
+package org.apache.http.auth;
+
+import java.util.Arrays;
+
+import org.apache.http.message.BasicNameValuePair;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestAuthChallenge {
+
+ @Test
+ public void testAuthChallengeWithValue() {
+ final AuthChallenge authChallenge = new AuthChallenge("Basic", "blah", null);
+ Assert.assertEquals("Basic", authChallenge.getScheme());
+ Assert.assertEquals("blah", authChallenge.getValue());
+ Assert.assertEquals(null, authChallenge.getParams());
+ Assert.assertEquals("Basic blah", authChallenge.toString());
+ }
+
+ @Test
+ public void testAuthChallengeWithParams() {
+ final AuthChallenge authChallenge = new AuthChallenge("Basic", null,
+ Arrays.asList(new BasicNameValuePair("blah", "this"), new BasicNameValuePair("blah", "that")));
+ Assert.assertEquals("Basic", authChallenge.getScheme());
+ Assert.assertEquals(null, authChallenge.getValue());
+ Assert.assertNotNull(authChallenge.getParams());
+ Assert.assertEquals("Basic [blah=this, blah=that]", authChallenge.toString());
+ }
+
+}
diff --git a/httpclient/src/test/java/org/apache/http/impl/auth/TestAuthChallengeParser.java b/httpclient/src/test/java/org/apache/http/impl/auth/TestAuthChallengeParser.java
new file mode 100644
index 000000000..65984d9ac
--- /dev/null
+++ b/httpclient/src/test/java/org/apache/http/impl/auth/TestAuthChallengeParser.java
@@ -0,0 +1,298 @@
+/*
+ * ====================================================================
+ * 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
+ * .
+ *
+ */
+package org.apache.http.impl.auth;
+
+import java.util.List;
+
+import org.apache.http.NameValuePair;
+import org.apache.http.ParseException;
+import org.apache.http.auth.AuthChallenge;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.message.ParserCursor;
+import org.apache.http.util.CharArrayBuffer;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class TestAuthChallengeParser {
+
+ private AuthChallengeParser parser;
+
+ @Before
+ public void setUp() throws Exception {
+ this.parser = new AuthChallengeParser();
+ }
+
+ @Test
+ public void testParseBasicToken() throws Exception {
+ final CharArrayBuffer buffer = new CharArrayBuffer(64);
+ buffer.append("blah");
+ final ParserCursor cursor = new ParserCursor(0, buffer.length());
+ final NameValuePair nvp = parser.parseTokenOrParameter(buffer, cursor);
+ Assert.assertNotNull(nvp);
+ Assert.assertEquals("blah", nvp.getName());
+ Assert.assertEquals(null, nvp.getValue());
+ }
+
+ @Test
+ public void testParseTokenWithBlanks() throws Exception {
+ final CharArrayBuffer buffer = new CharArrayBuffer(64);
+ buffer.append(" blah blah ");
+ final ParserCursor cursor = new ParserCursor(0, buffer.length());
+ final NameValuePair nvp = parser.parseTokenOrParameter(buffer, cursor);
+ Assert.assertNotNull(nvp);
+ Assert.assertEquals("blah", nvp.getName());
+ Assert.assertEquals(null, nvp.getValue());
+ }
+
+ @Test
+ public void testParseTokenDelimited() throws Exception {
+ final CharArrayBuffer buffer = new CharArrayBuffer(64);
+ buffer.append("blah,blah");
+ final ParserCursor cursor = new ParserCursor(0, buffer.length());
+ final NameValuePair nvp = parser.parseTokenOrParameter(buffer, cursor);
+ Assert.assertNotNull(nvp);
+ Assert.assertEquals("blah", nvp.getName());
+ Assert.assertEquals(null, nvp.getValue());
+ }
+
+ @Test
+ public void testParseParameterSimple() throws Exception {
+ final CharArrayBuffer buffer = new CharArrayBuffer(64);
+ buffer.append("param=blah");
+ final ParserCursor cursor = new ParserCursor(0, buffer.length());
+ final NameValuePair nvp = parser.parseTokenOrParameter(buffer, cursor);
+ Assert.assertNotNull(nvp);
+ Assert.assertEquals("param", nvp.getName());
+ Assert.assertEquals("blah", nvp.getValue());
+ }
+
+ @Test
+ public void testParseParameterDelimited() throws Exception {
+ final CharArrayBuffer buffer = new CharArrayBuffer(64);
+ buffer.append("param = blah , ");
+ final ParserCursor cursor = new ParserCursor(0, buffer.length());
+ final NameValuePair nvp = parser.parseTokenOrParameter(buffer, cursor);
+ Assert.assertNotNull(nvp);
+ Assert.assertEquals("param", nvp.getName());
+ Assert.assertEquals("blah", nvp.getValue());
+ }
+
+ @Test
+ public void testParseParameterQuoted() throws Exception {
+ final CharArrayBuffer buffer = new CharArrayBuffer(64);
+ buffer.append(" param = \" blah blah \"");
+ final ParserCursor cursor = new ParserCursor(0, buffer.length());
+ final NameValuePair nvp = parser.parseTokenOrParameter(buffer, cursor);
+ Assert.assertNotNull(nvp);
+ Assert.assertEquals("param", nvp.getName());
+ Assert.assertEquals(" blah blah ", nvp.getValue());
+ }
+
+ @Test
+ public void testParseParameterEscaped() throws Exception {
+ final CharArrayBuffer buffer = new CharArrayBuffer(64);
+ buffer.append(" param = \" blah \\\"blah\\\" \"");
+ final ParserCursor cursor = new ParserCursor(0, buffer.length());
+ final NameValuePair nvp = parser.parseTokenOrParameter(buffer, cursor);
+ Assert.assertNotNull(nvp);
+ Assert.assertEquals("param", nvp.getName());
+ Assert.assertEquals(" blah \"blah\" ", nvp.getValue());
+ }
+
+ @Test
+ public void testParseParameterNoValue() throws Exception {
+ final CharArrayBuffer buffer = new CharArrayBuffer(64);
+ buffer.append("param = , ");
+ final ParserCursor cursor = new ParserCursor(0, buffer.length());
+ final NameValuePair nvp = parser.parseTokenOrParameter(buffer, cursor);
+ Assert.assertNotNull(nvp);
+ Assert.assertEquals("param", nvp.getName());
+ Assert.assertEquals("", nvp.getValue());
+ }
+
+ @Test
+ public void testParseBasicAuthChallenge() throws Exception {
+ final CharArrayBuffer buffer = new CharArrayBuffer(64);
+ buffer.append("Basic realm=blah");
+ final ParserCursor cursor = new ParserCursor(0, buffer.length());
+ final List challenges = parser.parse(buffer, cursor);
+ Assert.assertNotNull(challenges);
+ Assert.assertEquals(1, challenges.size());
+ final AuthChallenge challenge1 = challenges.get(0);
+ Assert.assertEquals("Basic", challenge1.getScheme());
+ Assert.assertEquals(null, challenge1.getValue());
+ final List params = challenge1.getParams();
+ Assert.assertNotNull(params);
+ Assert.assertEquals(1, params.size());
+ Assert.assertEquals(new BasicNameValuePair("realm", "blah"), params.get(0));
+ }
+
+ @Test
+ public void testParseAuthChallengeWithBlanks() throws Exception {
+ final CharArrayBuffer buffer = new CharArrayBuffer(64);
+ buffer.append(" Basic realm = blah ");
+ final ParserCursor cursor = new ParserCursor(0, buffer.length());
+ final List challenges = parser.parse(buffer, cursor);
+ Assert.assertNotNull(challenges);
+ Assert.assertEquals(1, challenges.size());
+ final AuthChallenge challenge1 = challenges.get(0);
+ Assert.assertEquals("Basic", challenge1.getScheme());
+ Assert.assertEquals(null, challenge1.getValue());
+ final List params = challenge1.getParams();
+ Assert.assertNotNull(params);
+ Assert.assertEquals(1, params.size());
+ Assert.assertEquals(new BasicNameValuePair("realm", "blah"), params.get(0));
+ }
+
+ @Test
+ public void testParseMultipleAuthChallenge() throws Exception {
+ final CharArrayBuffer buffer = new CharArrayBuffer(64);
+ buffer.append("This xxxxxxxxxxxxxxxxxxxxxx, " +
+ "That yyyyyyyyyyyyyyyyyyyyyy ");
+ final ParserCursor cursor = new ParserCursor(0, buffer.length());
+ final List challenges = parser.parse(buffer, cursor);
+ Assert.assertNotNull(challenges);
+ Assert.assertEquals(2, challenges.size());
+
+ final AuthChallenge challenge1 = challenges.get(0);
+ Assert.assertEquals("This", challenge1.getScheme());
+ Assert.assertEquals("xxxxxxxxxxxxxxxxxxxxxx", challenge1.getValue());
+ Assert.assertNull(challenge1.getParams());
+
+ final AuthChallenge challenge2 = challenges.get(1);
+ Assert.assertEquals("That", challenge2.getScheme());
+ Assert.assertEquals("yyyyyyyyyyyyyyyyyyyyyy", challenge2.getValue());
+ Assert.assertNull(challenge2.getParams());
+ }
+
+ @Test
+ public void testParseMultipleAuthChallengeWithParams() throws Exception {
+ final CharArrayBuffer buffer = new CharArrayBuffer(64);
+ buffer.append("Basic realm=blah, param1 = this, param2=that, " +
+ "Basic realm=\"\\\"yada\\\"\", this, that=,this-and-that ");
+ final ParserCursor cursor = new ParserCursor(0, buffer.length());
+ final List challenges = parser.parse(buffer, cursor);
+ Assert.assertNotNull(challenges);
+ Assert.assertEquals(2, challenges.size());
+
+ final AuthChallenge challenge1 = challenges.get(0);
+ Assert.assertEquals("Basic", challenge1.getScheme());
+ Assert.assertEquals(null, challenge1.getValue());
+ final List params1 = challenge1.getParams();
+ Assert.assertNotNull(params1);
+ Assert.assertEquals(3, params1.size());
+ Assert.assertEquals(new BasicNameValuePair("realm", "blah"), params1.get(0));
+ Assert.assertEquals(new BasicNameValuePair("param1", "this"), params1.get(1));
+ Assert.assertEquals(new BasicNameValuePair("param2", "that"), params1.get(2));
+
+ final AuthChallenge challenge2 = challenges.get(1);
+ Assert.assertEquals("Basic", challenge2.getScheme());
+ Assert.assertEquals(null, challenge2.getValue());
+ final List params2 = challenge2.getParams();
+ Assert.assertNotNull(params2);
+ Assert.assertEquals(4, params2.size());
+ Assert.assertEquals(new BasicNameValuePair("realm", "\"yada\""), params2.get(0));
+ Assert.assertEquals(new BasicNameValuePair("this", null), params2.get(1));
+ Assert.assertEquals(new BasicNameValuePair("that", ""), params2.get(2));
+ Assert.assertEquals(new BasicNameValuePair("this-and-that", null), params2.get(3));
+ }
+
+ @Test
+ public void testParseEmptyAuthChallenge1() throws Exception {
+ final CharArrayBuffer buffer = new CharArrayBuffer(64);
+ buffer.append("This");
+ final ParserCursor cursor = new ParserCursor(0, buffer.length());
+ final List challenges = parser.parse(buffer, cursor);
+ Assert.assertNotNull(challenges);
+ Assert.assertEquals(1, challenges.size());
+
+ final AuthChallenge challenge1 = challenges.get(0);
+ Assert.assertEquals("This", challenge1.getScheme());
+ Assert.assertEquals(null, challenge1.getValue());
+ Assert.assertNull(challenge1.getParams());
+ }
+
+ @Test(expected = ParseException.class)
+ public void testParseMalformedAuthChallenge1() throws Exception {
+ final CharArrayBuffer buffer = new CharArrayBuffer(64);
+ buffer.append("This , ");
+ final ParserCursor cursor = new ParserCursor(0, buffer.length());
+ parser.parse(buffer, cursor);
+ }
+
+ @Test(expected = ParseException.class)
+ public void testParseMalformedAuthChallenge2() throws Exception {
+ final CharArrayBuffer buffer = new CharArrayBuffer(64);
+ buffer.append("This = that");
+ final ParserCursor cursor = new ParserCursor(0, buffer.length());
+ parser.parse(buffer, cursor);
+ }
+
+ @Test(expected = ParseException.class)
+ public void testParseMalformedAuthChallenge3() throws Exception {
+ final CharArrayBuffer buffer = new CharArrayBuffer(64);
+ buffer.append("blah blah blah");
+ final ParserCursor cursor = new ParserCursor(0, buffer.length());
+ parser.parse(buffer, cursor);
+ }
+
+ @Test
+ public void testParseValidAuthChallenge1() throws Exception {
+ final CharArrayBuffer buffer = new CharArrayBuffer(64);
+ buffer.append("blah blah");
+ final ParserCursor cursor = new ParserCursor(0, buffer.length());
+ final List challenges = parser.parse(buffer, cursor);
+ Assert.assertNotNull(challenges);
+ Assert.assertEquals(1, challenges.size());
+
+ final AuthChallenge challenge1 = challenges.get(0);
+ Assert.assertEquals("blah", challenge1.getScheme());
+ Assert.assertEquals("blah", challenge1.getValue());
+ Assert.assertNull(challenge1.getParams());
+ }
+
+ @Test
+ public void testParseValidAuthChallenge2() throws Exception {
+ final CharArrayBuffer buffer = new CharArrayBuffer(64);
+ buffer.append("blah blah, blah");
+ final ParserCursor cursor = new ParserCursor(0, buffer.length());
+ final List challenges = parser.parse(buffer, cursor);
+ Assert.assertNotNull(challenges);
+ Assert.assertEquals(1, challenges.size());
+
+ final AuthChallenge challenge1 = challenges.get(0);
+ Assert.assertEquals("blah", challenge1.getScheme());
+ Assert.assertEquals(null, challenge1.getValue());
+ final List params1 = challenge1.getParams();
+ Assert.assertNotNull(params1);
+ Assert.assertEquals(2, params1.size());
+ Assert.assertEquals(new BasicNameValuePair("blah", null), params1.get(0));
+ Assert.assertEquals(new BasicNameValuePair("blah", null), params1.get(1));
+ }
+
+}