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