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       * @since 4.3
85       */
86      public BasicScheme(final Charset charset) {
87          this.paramMap = new HashMap<>();
88          this.defaultCharset = charset != null ? charset : StandardCharsets.US_ASCII;
89          this.complete = false;
90      }
91  
92      public BasicScheme() {
93          this(StandardCharsets.US_ASCII);
94      }
95  
96      public void initPreemptive(final Credentials credentials) {
97          if (credentials != null) {
98              Args.check(credentials instanceof UsernamePasswordCredentials,
99                      "Unsupported credential type: " + credentials.getClass());
100             this.credentials = (UsernamePasswordCredentials) credentials;
101         } else {
102             this.credentials = null;
103         }
104     }
105 
106     @Override
107     public String getName() {
108         return StandardAuthScheme.BASIC;
109     }
110 
111     @Override
112     public boolean isConnectionBased() {
113         return false;
114     }
115 
116     @Override
117     public String getRealm() {
118         return this.paramMap.get("realm");
119     }
120 
121     @Override
122     public void processChallenge(
123             final AuthChallenge authChallenge,
124             final HttpContext context) throws MalformedChallengeException {
125         this.paramMap.clear();
126         final List<NameValuePair> params = authChallenge.getParams();
127         if (params != null) {
128             for (final NameValuePair param: params) {
129                 this.paramMap.put(param.getName().toLowerCase(Locale.ROOT), param.getValue());
130             }
131         }
132         this.complete = true;
133     }
134 
135     @Override
136     public boolean isChallengeComplete() {
137         return this.complete;
138     }
139 
140     @Override
141     public boolean isResponseReady(
142             final HttpHost host,
143             final CredentialsProvider credentialsProvider,
144             final HttpContext context) throws AuthenticationException {
145 
146         Args.notNull(host, "Auth host");
147         Args.notNull(credentialsProvider, "CredentialsProvider");
148 
149         final AuthScope authScope = new AuthScope(host, getRealm(), getName());
150         final Credentials credentials = credentialsProvider.getCredentials(
151                 authScope, context);
152         if (credentials instanceof UsernamePasswordCredentials) {
153             this.credentials = (UsernamePasswordCredentials) credentials;
154             return true;
155         }
156 
157         if (LOG.isDebugEnabled()) {
158             final HttpClientContext clientContext = HttpClientContext.adapt(context);
159             final String exchangeId = clientContext.getExchangeId();
160             LOG.debug("{} No credentials found for auth scope [{}]", exchangeId, authScope);
161         }
162         this.credentials = null;
163         return false;
164     }
165 
166     @Override
167     public Principal getPrincipal() {
168         return null;
169     }
170 
171     private void validateUsername() throws AuthenticationException {
172         if (credentials == null) {
173             throw new AuthenticationException("User credentials not set");
174         }
175         final String username = credentials.getUserName();
176         for (int i = 0; i < username.length(); i++) {
177             final char ch = username.charAt(i);
178             if (Character.isISOControl(ch)) {
179                 throw new AuthenticationException("Username must not contain any control characters");
180             }
181             if (ch == ':') {
182                 throw new AuthenticationException("Username contains a colon character and is invalid");
183             }
184         }
185     }
186 
187     @Override
188     public String generateAuthResponse(
189             final HttpHost host,
190             final HttpRequest request,
191             final HttpContext context) throws AuthenticationException {
192         validateUsername();
193         if (this.buffer == null) {
194             this.buffer = new ByteArrayBuilder(64);
195         } else {
196             this.buffer.reset();
197         }
198         final Charset charset = AuthSchemeSupport.parseCharset(paramMap.get("charset"), defaultCharset);
199         this.buffer.charset(charset);
200         this.buffer.append(this.credentials.getUserName()).append(":").append(this.credentials.getUserPassword());
201         if (this.base64codec == null) {
202             this.base64codec = new Base64();
203         }
204         final byte[] encodedCreds = this.base64codec.encode(this.buffer.toByteArray());
205         this.buffer.reset();
206         return StandardAuthScheme.BASIC + " " + new String(encodedCreds, 0, encodedCreds.length, StandardCharsets.US_ASCII);
207     }
208 
209     private void writeObject(final ObjectOutputStream out) throws IOException {
210         out.defaultWriteObject();
211         out.writeUTF(this.defaultCharset.name());
212     }
213 
214     @SuppressWarnings("unchecked")
215     private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
216         in.defaultReadObject();
217         try {
218             this.defaultCharset = Charset.forName(in.readUTF());
219         } catch (final UnsupportedCharsetException ex) {
220             this.defaultCharset = StandardCharsets.US_ASCII;
221         }
222     }
223 
224     private void readObjectNoData() {
225     }
226 
227     @Override
228     public String toString() {
229         return getName() + this.paramMap;
230     }
231 
232 }