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  package org.apache.hc.client5.http.impl.auth;
28  
29  import java.io.Serializable;
30  import java.nio.charset.Charset;
31  import java.nio.charset.StandardCharsets;
32  import java.security.Principal;
33  import java.util.HashMap;
34  import java.util.List;
35  import java.util.Locale;
36  import java.util.Map;
37  
38  import org.apache.hc.client5.http.auth.AuthChallenge;
39  import org.apache.hc.client5.http.auth.AuthScheme;
40  import org.apache.hc.client5.http.auth.AuthScope;
41  import org.apache.hc.client5.http.auth.AuthStateCacheable;
42  import org.apache.hc.client5.http.auth.AuthenticationException;
43  import org.apache.hc.client5.http.auth.Credentials;
44  import org.apache.hc.client5.http.auth.CredentialsProvider;
45  import org.apache.hc.client5.http.auth.MalformedChallengeException;
46  import org.apache.hc.client5.http.auth.StandardAuthScheme;
47  import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
48  import org.apache.hc.client5.http.impl.StateHolder;
49  import org.apache.hc.client5.http.protocol.HttpClientContext;
50  import org.apache.hc.client5.http.utils.Base64;
51  import org.apache.hc.client5.http.utils.ByteArrayBuilder;
52  import org.apache.hc.core5.annotation.Internal;
53  import org.apache.hc.core5.http.HttpHost;
54  import org.apache.hc.core5.http.HttpRequest;
55  import org.apache.hc.core5.http.NameValuePair;
56  import org.apache.hc.core5.http.protocol.HttpContext;
57  import org.apache.hc.core5.util.Args;
58  import org.slf4j.Logger;
59  import org.slf4j.LoggerFactory;
60  
61  /**
62   * Basic authentication scheme.
63   *
64   * @since 4.0
65   */
66  @AuthStateCacheable
67  public class BasicScheme implements AuthScheme, StateHolder<BasicScheme.State>, Serializable {
68  
69      private static final long serialVersionUID = -1931571557597830536L;
70  
71      private static final Logger LOG = LoggerFactory.getLogger(BasicScheme.class);
72  
73      private final Map<String, String> paramMap;
74      private transient ByteArrayBuilder buffer;
75      private transient Base64 base64codec;
76      private boolean complete;
77  
78      private UsernamePasswordCredentials credentials;
79  
80      /**
81       * @deprecated This constructor is deprecated to enforce the use of {@link StandardCharsets#UTF_8} encoding
82       * in compliance with RFC 7617 for HTTP Basic Authentication. Use the default constructor {@link #BasicScheme()} instead.
83       *
84       * @param charset the {@link Charset} set to be used for encoding credentials. This parameter is ignored as UTF-8 is always used.
85       */
86      @Deprecated
87      public BasicScheme(final Charset charset) {
88          this.paramMap = new HashMap<>();
89          this.complete = false;
90      }
91  
92      /**
93       * Constructs a new BasicScheme with UTF-8 as the charset.
94       *
95       * @since 4.3
96       */
97      public BasicScheme() {
98          this.paramMap = new HashMap<>();
99          this.complete = false;
100     }
101 
102     public void initPreemptive(final Credentials credentials) {
103         if (credentials != null) {
104             Args.check(credentials instanceof UsernamePasswordCredentials,
105                     "Unsupported credential type: " + credentials.getClass());
106             this.credentials = (UsernamePasswordCredentials) credentials;
107             this.complete = true;
108         } else {
109             this.credentials = null;
110         }
111     }
112 
113     @Override
114     public String getName() {
115         return StandardAuthScheme.BASIC;
116     }
117 
118     @Override
119     public boolean isConnectionBased() {
120         return false;
121     }
122 
123     @Override
124     public String getRealm() {
125         return this.paramMap.get("realm");
126     }
127 
128     @Override
129     public void processChallenge(
130             final AuthChallenge authChallenge,
131             final HttpContext context) throws MalformedChallengeException {
132         this.paramMap.clear();
133         final List<NameValuePair> params = authChallenge.getParams();
134         if (params != null) {
135             for (final NameValuePair param: params) {
136                 this.paramMap.put(param.getName().toLowerCase(Locale.ROOT), param.getValue());
137             }
138         }
139         this.complete = true;
140     }
141 
142     @Override
143     public boolean isChallengeComplete() {
144         return this.complete;
145     }
146 
147     @Override
148     public boolean isResponseReady(
149             final HttpHost host,
150             final CredentialsProvider credentialsProvider,
151             final HttpContext context) throws AuthenticationException {
152 
153         Args.notNull(host, "Auth host");
154         Args.notNull(credentialsProvider, "CredentialsProvider");
155 
156         final AuthScope authScope = new AuthScope(host, getRealm(), getName());
157         final Credentials credentials = credentialsProvider.getCredentials(
158                 authScope, context);
159         if (credentials instanceof UsernamePasswordCredentials) {
160             this.credentials = (UsernamePasswordCredentials) credentials;
161             return true;
162         }
163 
164         if (LOG.isDebugEnabled()) {
165             final HttpClientContext clientContext = HttpClientContext.cast(context);
166             final String exchangeId = clientContext.getExchangeId();
167             LOG.debug("{} No credentials found for auth scope [{}]", exchangeId, authScope);
168         }
169         this.credentials = null;
170         return false;
171     }
172 
173     @Override
174     public Principal getPrincipal() {
175         return null;
176     }
177 
178     private void validateUsername() throws AuthenticationException {
179         if (credentials == null) {
180             throw new AuthenticationException("User credentials not set");
181         }
182         final String username = credentials.getUserName();
183         for (int i = 0; i < username.length(); i++) {
184             final char ch = username.charAt(i);
185             if (Character.isISOControl(ch)) {
186                 throw new AuthenticationException("Username must not contain any control characters");
187             }
188             if (ch == ':') {
189                 throw new AuthenticationException("Username contains a colon character and is invalid");
190             }
191         }
192     }
193 
194     private void validatePassword() throws AuthenticationException {
195         if (credentials == null) {
196             throw new AuthenticationException("User credentials not set");
197         }
198         final char[] password = credentials.getUserPassword();
199         if (password != null) {
200             for (final char ch : password) {
201                 if (Character.isISOControl(ch)) {
202                     throw new AuthenticationException("Password must not contain any control characters");
203                 }
204             }
205         }
206     }
207 
208     @Override
209     public String generateAuthResponse(
210             final HttpHost host,
211             final HttpRequest request,
212             final HttpContext context) throws AuthenticationException {
213         validateUsername();
214         validatePassword();
215         if (this.buffer == null) {
216             this.buffer = new ByteArrayBuilder(64);
217         } else {
218             this.buffer.reset();
219         }
220         final Charset charset = AuthSchemeSupport.parseCharset(paramMap.get("charset"), StandardCharsets.UTF_8);
221         this.buffer.charset(charset);
222         this.buffer.append(this.credentials.getUserName()).append(":").append(this.credentials.getUserPassword());
223         if (this.base64codec == null) {
224             this.base64codec = new Base64();
225         }
226         final byte[] encodedCreds = this.base64codec.encode(this.buffer.toByteArray());
227         this.buffer.reset();
228         return StandardAuthScheme.BASIC + " " + new String(encodedCreds, 0, encodedCreds.length, StandardCharsets.US_ASCII);
229     }
230 
231     @Override
232     public State store() {
233         if (complete) {
234             return new State(new HashMap<>(paramMap), credentials);
235         } else {
236             return null;
237         }
238     }
239 
240     @Override
241     public void restore(final State state) {
242         if (state != null) {
243             paramMap.clear();
244             paramMap.putAll(state.params);
245             credentials = state.credentials;
246             complete = true;
247         }
248     }
249 
250     @Override
251     public String toString() {
252         return getName() + this.paramMap;
253     }
254 
255     @Internal
256     public static class State {
257 
258         final Map<String, String> params;
259         final UsernamePasswordCredentials credentials;
260 
261         State(final Map<String, String> params, final UsernamePasswordCredentials credentials) {
262             this.params = params;
263             this.credentials = credentials;
264         }
265 
266         @Override
267         public String toString() {
268             return "State{" +
269                     "params=" + params +
270                     '}';
271         }
272     }
273 
274 }