View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  
28  package org.apache.hc.client5.http.impl.auth;
29  
30  import java.util.ArrayList;
31  import java.util.BitSet;
32  import java.util.List;
33  
34  import org.apache.hc.client5.http.auth.AuthChallenge;
35  import org.apache.hc.client5.http.auth.ChallengeType;
36  import org.apache.hc.core5.http.NameValuePair;
37  import org.apache.hc.core5.http.ParseException;
38  import org.apache.hc.core5.http.message.BasicNameValuePair;
39  import org.apache.hc.core5.http.message.ParserCursor;
40  import org.apache.hc.core5.util.TextUtils;
41  import org.apache.hc.core5.util.Tokenizer;
42  
43  /**
44   * Authentication challenge parser.
45   *
46   * @since 5.0
47   */
48  public class AuthChallengeParser {
49  
50      public static final AuthChallengeParser INSTANCE = new AuthChallengeParser();
51  
52      private final Tokenizer tokenParser = Tokenizer.INSTANCE;
53  
54      private final static char BLANK            = ' ';
55      private final static char COMMA_CHAR       = ',';
56      private final static char EQUAL_CHAR       = '=';
57  
58      // IMPORTANT!
59      // These private static variables must be treated as immutable and never exposed outside this class
60      private static final BitSet TERMINATORS = Tokenizer.INIT_BITSET(BLANK, EQUAL_CHAR, COMMA_CHAR);
61      private static final BitSet DELIMITER = Tokenizer.INIT_BITSET(COMMA_CHAR);
62      private static final BitSet SPACE = Tokenizer.INIT_BITSET(BLANK);
63  
64      static class ChallengeInt {
65  
66          final String schemeName;
67          final List<NameValuePair> params;
68  
69          ChallengeInt(final String schemeName) {
70              this.schemeName = schemeName;
71              this.params = new ArrayList<>();
72          }
73  
74          @Override
75          public String toString() {
76              return "ChallengeInternal{" +
77                      "schemeName='" + schemeName + '\'' +
78                      ", params=" + params +
79                      '}';
80          }
81  
82      }
83  
84      /**
85       * Parses the given sequence of characters into a list of {@link AuthChallenge} elements.
86       *
87       * @param challengeType the type of challenge (target or proxy).
88       * @param buffer the sequence of characters to be parsed.
89       * @param cursor the parser cursor.
90       * @return a list of auth challenge elements.
91       */
92      public List<AuthChallenge> parse(
93              final ChallengeType challengeType, final CharSequence buffer, final ParserCursor cursor) throws ParseException {
94          tokenParser.skipWhiteSpace(buffer, cursor);
95          if (cursor.atEnd()) {
96              throw new ParseException("Malformed auth challenge");
97          }
98          final List<ChallengeInt> internalChallenges = new ArrayList<>();
99          final String schemeName = tokenParser.parseToken(buffer, cursor, SPACE);
100         if (TextUtils.isBlank(schemeName)) {
101             throw new ParseException("Malformed auth challenge");
102         }
103         ChallengeInt current = new ChallengeInt(schemeName);
104         while (current != null) {
105             internalChallenges.add(current);
106             current = parseChallenge(buffer, cursor, current);
107         }
108         final List<AuthChallenge> challenges = new ArrayList<>(internalChallenges.size());
109         for (final ChallengeInt internal : internalChallenges) {
110             final List<NameValuePair> params = internal.params;
111             String token68 = null;
112             if (params.size() == 1) {
113                 final NameValuePair param = params.get(0);
114                 if (param.getValue() == null) {
115                     token68 = param.getName();
116                     params.clear();
117                 }
118             }
119             challenges.add(
120                     new AuthChallenge(challengeType, internal.schemeName, token68, !params.isEmpty() ? params : null));
121         }
122         return challenges;
123     }
124 
125     ChallengeInt parseChallenge(
126             final CharSequence buffer,
127             final ParserCursor cursor,
128             final ChallengeInt currentChallenge) throws ParseException {
129         for (;;) {
130             tokenParser.skipWhiteSpace(buffer, cursor);
131             if (cursor.atEnd()) {
132                 return null;
133             }
134             final String token = parseToken(buffer, cursor);
135             if (TextUtils.isBlank(token)) {
136                 throw new ParseException("Malformed auth challenge");
137             }
138             tokenParser.skipWhiteSpace(buffer, cursor);
139 
140             // it gets really messy here
141             if (cursor.atEnd()) {
142                 // at the end of the header
143                 currentChallenge.params.add(new BasicNameValuePair(token, null));
144             } else {
145                 char ch = buffer.charAt(cursor.getPos());
146                 if (ch == EQUAL_CHAR) {
147                     cursor.updatePos(cursor.getPos() + 1);
148                     final String value = tokenParser.parseValue(buffer, cursor, DELIMITER);
149                     tokenParser.skipWhiteSpace(buffer, cursor);
150                     if (!cursor.atEnd()) {
151                         ch = buffer.charAt(cursor.getPos());
152                         if (ch == COMMA_CHAR) {
153                             cursor.updatePos(cursor.getPos() + 1);
154                         }
155                     }
156                     currentChallenge.params.add(new BasicNameValuePair(token, value));
157                 } else if (ch == COMMA_CHAR) {
158                     cursor.updatePos(cursor.getPos() + 1);
159                     currentChallenge.params.add(new BasicNameValuePair(token, null));
160                 } else {
161                     // the token represents new challenge
162                     if (currentChallenge.params.isEmpty()) {
163                         throw new ParseException("Malformed auth challenge");
164                     }
165                     return new ChallengeInt(token);
166                 }
167             }
168         }
169     }
170 
171     String parseToken(final CharSequence buf, final ParserCursor cursor) {
172         final StringBuilder dst = new StringBuilder();
173         while (!cursor.atEnd()) {
174             int pos = cursor.getPos();
175             char current = buf.charAt(pos);
176             if (TERMINATORS.get(current)) {
177                 // Here it gets really ugly
178                 if (current == EQUAL_CHAR) {
179                     // it can be a start of a parameter value or token68 padding
180                     // Look ahead and see if there are more '=' or at end of buffer
181                     if (pos + 1 < cursor.getUpperBound() && buf.charAt(pos + 1) != EQUAL_CHAR) {
182                         break;
183                     }
184                     do {
185                         dst.append(current);
186                         pos++;
187                         cursor.updatePos(pos);
188                         if (cursor.atEnd()) {
189                             break;
190                         }
191                         current = buf.charAt(pos);
192                     } while (current == EQUAL_CHAR);
193                 } else {
194                     break;
195                 }
196             } else {
197                 dst.append(current);
198                 cursor.updatePos(pos + 1);
199             }
200         }
201         return dst.toString();
202     }
203 
204 }