blob: aef15f6b28a5ac110081c44a418441fb9bdb28c9 [file] [log] [blame]
Hesam Rahimi4a409b42016-08-12 18:37:33 -04001/*
Brian O'Connora09fe5b2017-08-03 21:12:30 -07002 * Copyright 2016-present Open Networking Foundation
Hesam Rahimi4a409b42016-08-12 18:37:33 -04003 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package org.onosproject.protocol.http.ctl;
18
Michal Machbcd58c72017-06-19 17:12:34 +020019import com.google.common.collect.ImmutableMap;
Hesam Rahimi4a409b42016-08-12 18:37:33 -040020import org.apache.commons.io.IOUtils;
21import org.apache.http.client.methods.HttpPatch;
22import org.apache.http.conn.ssl.AllowAllHostnameVerifier;
23import org.apache.http.entity.StringEntity;
24import org.apache.http.impl.client.CloseableHttpClient;
25import org.apache.http.impl.client.HttpClients;
26import org.apache.http.ssl.SSLContextBuilder;
27import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
fahadnaeemkhan02ffa712017-12-01 19:49:45 -080028import org.glassfish.jersey.client.oauth2.OAuth2ClientSupport;
Hesam Rahimi4a409b42016-08-12 18:37:33 -040029import org.onlab.packet.IpAddress;
30import org.onosproject.net.DeviceId;
31import org.onosproject.protocol.http.HttpSBController;
32import org.onosproject.protocol.rest.RestSBDevice;
fahadnaeemkhan02ffa712017-12-01 19:49:45 -080033import org.onosproject.protocol.rest.RestSBDevice.AuthenticationScheme;
Hesam Rahimi4a409b42016-08-12 18:37:33 -040034import org.slf4j.Logger;
35import org.slf4j.LoggerFactory;
36
Michal Machbcd58c72017-06-19 17:12:34 +020037import javax.net.ssl.SSLContext;
38import javax.net.ssl.TrustManager;
39import javax.net.ssl.X509TrustManager;
40import javax.ws.rs.client.Client;
41import javax.ws.rs.client.ClientBuilder;
42import javax.ws.rs.client.Entity;
43import javax.ws.rs.client.WebTarget;
44import javax.ws.rs.core.MediaType;
45import javax.ws.rs.core.Response;
46import javax.ws.rs.core.Response.Status;
47import java.io.ByteArrayInputStream;
48import java.io.IOException;
49import java.io.InputStream;
50import java.nio.charset.StandardCharsets;
51import java.security.KeyManagementException;
52import java.security.KeyStoreException;
53import java.security.NoSuchAlgorithmException;
54import java.security.cert.CertificateException;
55import java.security.cert.X509Certificate;
56import java.util.Base64;
57import java.util.Map;
58import java.util.concurrent.ConcurrentHashMap;
Hesam Rahimi4a409b42016-08-12 18:37:33 -040059
fahadnaeemkhan02ffa712017-12-01 19:49:45 -080060import static com.google.common.base.Preconditions.checkNotNull;
61
Hesam Rahimi4a409b42016-08-12 18:37:33 -040062/**
63 * The implementation of HttpSBController.
64 */
65public class HttpSBControllerImpl implements HttpSBController {
66
Matteo Gerola7e180c22017-03-30 11:57:58 +020067 private static final Logger log = LoggerFactory.getLogger(HttpSBControllerImpl.class);
Hesam Rahimi4a409b42016-08-12 18:37:33 -040068 private static final String XML = "xml";
69 private static final String JSON = "json";
Michele Santuaric372c222017-01-12 09:41:25 +010070 protected static final String DOUBLESLASH = "//";
71 protected static final String COLON = ":";
Hesam Rahimi4a409b42016-08-12 18:37:33 -040072 private static final int STATUS_OK = Response.Status.OK.getStatusCode();
73 private static final int STATUS_CREATED = Response.Status.CREATED.getStatusCode();
74 private static final int STATUS_ACCEPTED = Response.Status.ACCEPTED.getStatusCode();
75 private static final String HTTPS = "https";
76 private static final String AUTHORIZATION_PROPERTY = "authorization";
77 private static final String BASIC_AUTH_PREFIX = "Basic ";
fahadnaeemkhan2675a272017-12-13 13:17:23 -080078 private static final String OAUTH2_BEARER_AUTH_PREFIX = "Bearer ";
Hesam Rahimi4a409b42016-08-12 18:37:33 -040079
80 private final Map<DeviceId, RestSBDevice> deviceMap = new ConcurrentHashMap<>();
81 private final Map<DeviceId, Client> clientMap = new ConcurrentHashMap<>();
82
83 public Map<DeviceId, RestSBDevice> getDeviceMap() {
84 return deviceMap;
85 }
86
87 public Map<DeviceId, Client> getClientMap() {
88 return clientMap;
89 }
90
91 @Override
92 public Map<DeviceId, RestSBDevice> getDevices() {
93 return ImmutableMap.copyOf(deviceMap);
94 }
95
96 @Override
97 public RestSBDevice getDevice(DeviceId deviceInfo) {
98 return deviceMap.get(deviceInfo);
99 }
100
101 @Override
102 public RestSBDevice getDevice(IpAddress ip, int port) {
Matteo Gerola7e180c22017-03-30 11:57:58 +0200103 return deviceMap.values().stream().filter(v -> v.ip().equals(ip) && v.port() == port).findFirst().get();
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400104 }
105
106 @Override
107 public void addDevice(RestSBDevice device) {
108 if (!deviceMap.containsKey(device.deviceId())) {
109 Client client = ignoreSslClient();
fahadnaeemkhan02ffa712017-12-01 19:49:45 -0800110 authenticate(client, device);
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400111 clientMap.put(device.deviceId(), client);
112 deviceMap.put(device.deviceId(), device);
113 } else {
114 log.warn("Trying to add a device that is already existing {}", device.deviceId());
115 }
116
117 }
118
119 @Override
120 public void removeDevice(DeviceId deviceId) {
121 clientMap.remove(deviceId);
122 deviceMap.remove(deviceId);
123 }
124
125 @Override
Matteo Gerola7e180c22017-03-30 11:57:58 +0200126 public int post(DeviceId device, String request, InputStream payload, MediaType mediaType) {
127 Response response = getResponse(device, request, payload, mediaType);
128 if (response == null) {
129 return Status.NO_CONTENT.getStatusCode();
130 }
131 return response.getStatus();
132 }
133
134 @Override
Matteo Gerola7e180c22017-03-30 11:57:58 +0200135 public <T> T post(DeviceId device, String request, InputStream payload, MediaType mediaType,
136 Class<T> responseClass) {
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400137 Response response = getResponse(device, request, payload, mediaType);
Palash Kalada4798d2017-05-23 20:16:55 +0900138 if (response != null && response.hasEntity()) {
Hesam Rahimi96305542017-06-07 13:59:48 -0400139 // Do not read the entity if the responseClass is of type Response. This would allow the
140 // caller to receive the Response directly and try to read its appropriate entity locally.
141 return responseClass == Response.class ? (T) response : response.readEntity(responseClass);
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400142 }
143 log.error("Response from device {} for request {} contains no entity", device, request);
144 return null;
145 }
146
Matteo Gerola7e180c22017-03-30 11:57:58 +0200147 private Response getResponse(DeviceId device, String request, InputStream payload, MediaType mediaType) {
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400148
149 WebTarget wt = getWebTarget(device, request);
150
151 Response response = null;
152 if (payload != null) {
153 try {
Eunjin Choi51244d32017-05-15 14:09:56 +0900154 response = wt.request(mediaType)
155 .post(Entity.entity(IOUtils.toString(payload, StandardCharsets.UTF_8), mediaType));
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400156 } catch (IOException e) {
Matteo Gerola7e180c22017-03-30 11:57:58 +0200157 log.error("Cannot do POST {} request on device {} because can't read payload", request, device);
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400158 }
159 } else {
Georgios Katsikas186b9582017-05-31 17:25:54 +0200160 response = wt.request(mediaType).post(Entity.entity(null, mediaType));
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400161 }
162 return response;
163 }
164
165 @Override
Matteo Gerola7e180c22017-03-30 11:57:58 +0200166 public int put(DeviceId device, String request, InputStream payload, MediaType mediaType) {
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400167
168 WebTarget wt = getWebTarget(device, request);
169
170 Response response = null;
171 if (payload != null) {
172 try {
Eunjin Choi51244d32017-05-15 14:09:56 +0900173 response = wt.request(mediaType).put(Entity.entity(IOUtils.
174 toString(payload, StandardCharsets.UTF_8), mediaType));
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400175 } catch (IOException e) {
Matteo Gerola7e180c22017-03-30 11:57:58 +0200176 log.error("Cannot do PUT {} request on device {} because can't read payload", request, device);
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400177 }
178 } else {
Eunjin Choi51244d32017-05-15 14:09:56 +0900179 response = wt.request(mediaType).put(Entity.entity(null, mediaType));
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400180 }
Matteo Gerola7e180c22017-03-30 11:57:58 +0200181
182 if (response == null) {
183 return Status.NO_CONTENT.getStatusCode();
184 }
185 return response.getStatus();
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400186 }
187
188 @Override
Matteo Gerola7e180c22017-03-30 11:57:58 +0200189 public InputStream get(DeviceId device, String request, MediaType mediaType) {
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400190 WebTarget wt = getWebTarget(device, request);
191
Eunjin Choi51244d32017-05-15 14:09:56 +0900192 Response s = wt.request(mediaType).get();
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400193
194 if (checkReply(s)) {
Matteo Gerola7e180c22017-03-30 11:57:58 +0200195 return new ByteArrayInputStream(s.readEntity((String.class)).getBytes(StandardCharsets.UTF_8));
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400196 }
197 return null;
198 }
199
200 @Override
Matteo Gerola7e180c22017-03-30 11:57:58 +0200201 public int patch(DeviceId device, String request, InputStream payload, MediaType mediaType) {
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400202
203 try {
204 log.debug("Url request {} ", getUrlString(device, request));
205 HttpPatch httprequest = new HttpPatch(getUrlString(device, request));
fahadnaeemkhan2675a272017-12-13 13:17:23 -0800206 if (deviceMap.get(device).authentication() == AuthenticationScheme.BASIC) {
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400207 String pwd = deviceMap.get(device).password() == null ? "" : COLON + deviceMap.get(device).password();
208 String userPassword = deviceMap.get(device).username() + pwd;
209 String base64string = Base64.getEncoder().encodeToString(userPassword.getBytes(StandardCharsets.UTF_8));
210 httprequest.addHeader(AUTHORIZATION_PROPERTY, BASIC_AUTH_PREFIX + base64string);
fahadnaeemkhan2675a272017-12-13 13:17:23 -0800211 } else if (deviceMap.get(device).authentication() == AuthenticationScheme.OAUTH2) {
212 String token = deviceMap.get(device).token();
213 // TODO: support token types other then bearer of OAuth2 authentication
214 httprequest.addHeader(AUTHORIZATION_PROPERTY, OAUTH2_BEARER_AUTH_PREFIX + token);
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400215 }
216 if (payload != null) {
217 StringEntity input = new StringEntity(IOUtils.toString(payload, StandardCharsets.UTF_8));
Eunjin Choi51244d32017-05-15 14:09:56 +0900218 input.setContentType(mediaType.toString());
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400219 httprequest.setEntity(input);
220 }
221 CloseableHttpClient httpClient;
222 if (deviceMap.containsKey(device) && deviceMap.get(device).protocol().equals(HTTPS)) {
223 httpClient = getApacheSslBypassClient();
224 } else {
225 httpClient = HttpClients.createDefault();
226 }
Matteo Gerola7e180c22017-03-30 11:57:58 +0200227 return httpClient.execute(httprequest).getStatusLine().getStatusCode();
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400228 } catch (IOException | NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) {
Matteo Gerola7e180c22017-03-30 11:57:58 +0200229 log.error("Cannot do PATCH {} request on device {}", request, device, e);
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400230 }
Matteo Gerola7e180c22017-03-30 11:57:58 +0200231 return Status.BAD_REQUEST.getStatusCode();
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400232 }
233
234 @Override
Matteo Gerola7e180c22017-03-30 11:57:58 +0200235 public int delete(DeviceId device, String request, InputStream payload, MediaType mediaType) {
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400236
237 WebTarget wt = getWebTarget(device, request);
238
Matteo Gerola7e180c22017-03-30 11:57:58 +0200239 // FIXME: do we need to delete an entry by enclosing data in DELETE
240 // request?
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400241 // wouldn't it be nice to use PUT to implement the similar concept?
Eunjin Choi51244d32017-05-15 14:09:56 +0900242 Response response = wt.request(mediaType).delete();
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400243
Matteo Gerola7e180c22017-03-30 11:57:58 +0200244 return response.getStatus();
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400245 }
246
Matteo Gerola7e180c22017-03-30 11:57:58 +0200247 private MediaType typeOfMediaType(String type) {
248 switch (type) {
249 case XML:
250 return MediaType.APPLICATION_XML_TYPE;
251 case JSON:
252 return MediaType.APPLICATION_JSON_TYPE;
Michal Machf0ce45e2017-06-20 11:54:08 +0200253 case MediaType.WILDCARD:
254 return MediaType.WILDCARD_TYPE;
Matteo Gerola7e180c22017-03-30 11:57:58 +0200255 default:
256 throw new IllegalArgumentException("Unsupported media type " + type);
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400257
258 }
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400259 }
260
fahadnaeemkhan02ffa712017-12-01 19:49:45 -0800261 private void authenticate(Client client, RestSBDevice device) {
262 AuthenticationScheme authScheme = device.authentication();
263 if (authScheme == AuthenticationScheme.NO_AUTHENTICATION) {
264 log.debug("{} scheme is specified, ignoring authentication", authScheme);
265 return;
266 } else if (authScheme == AuthenticationScheme.OAUTH2) {
267 String token = checkNotNull(device.token());
268 client.register(OAuth2ClientSupport.feature(token));
269 } else if (authScheme == AuthenticationScheme.BASIC) {
270 String username = device.username();
271 String password = device.password() == null ? "" : device.password();
272 client.register(HttpAuthenticationFeature.basic(username, password));
273 } else {
274 // TODO: Add support for other authentication schemes here.
275 throw new IllegalArgumentException(String.format("Unsupported authentication scheme: %s",
276 authScheme.name()));
277 }
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400278 }
279
280 protected WebTarget getWebTarget(DeviceId device, String request) {
281 log.debug("Sending request to URL {} ", getUrlString(device, request));
282 return clientMap.get(device).target(getUrlString(device, request));
283 }
284
285 //FIXME security issue: this trusts every SSL certificate, even if is self-signed. Also deprecated methods.
286 private CloseableHttpClient getApacheSslBypassClient() throws NoSuchAlgorithmException,
287 KeyManagementException, KeyStoreException {
288 return HttpClients.custom().
289 setHostnameVerifier(new AllowAllHostnameVerifier()).
290 setSslcontext(new SSLContextBuilder()
291 .loadTrustMaterial(null, (arg0, arg1) -> true)
292 .build()).build();
293 }
294
Michal Machbcd58c72017-06-19 17:12:34 +0200295 protected String getUrlString(DeviceId deviceId, String request) {
296 RestSBDevice restSBDevice = deviceMap.get(deviceId);
297 if (restSBDevice == null) {
298 log.warn("restSbDevice cannot be NULL!");
299 return "";
300 }
301 if (restSBDevice.url() != null) {
302 return restSBDevice.protocol() + COLON + DOUBLESLASH + restSBDevice.url() + request;
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400303 } else {
Michal Machbcd58c72017-06-19 17:12:34 +0200304 return restSBDevice.protocol() + COLON + DOUBLESLASH + restSBDevice.ip().toString()
305 + COLON + restSBDevice.port() + request;
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400306 }
307 }
308
309 private boolean checkReply(Response response) {
310 if (response != null) {
Matteo Gerola7e180c22017-03-30 11:57:58 +0200311 boolean statusCode = checkStatusCode(response.getStatus());
312 if (!statusCode && response.hasEntity()) {
313 log.error("Failed request, HTTP error msg : " + response.readEntity(String.class));
314 }
315 return statusCode;
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400316 }
317 log.error("Null reply from device");
318 return false;
319 }
320
321 private boolean checkStatusCode(int statusCode) {
Matteo Gerola7e180c22017-03-30 11:57:58 +0200322 if (statusCode == STATUS_OK || statusCode == STATUS_CREATED || statusCode == STATUS_ACCEPTED) {
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400323 return true;
324 } else {
Matteo Gerola7e180c22017-03-30 11:57:58 +0200325 log.error("Failed request, HTTP error code : " + statusCode);
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400326 return false;
327 }
328 }
329
330 private Client ignoreSslClient() {
331 SSLContext sslcontext = null;
332
333 try {
334 sslcontext = SSLContext.getInstance("TLS");
335 sslcontext.init(null, new TrustManager[]{new X509TrustManager() {
fahadnaeemkhan02ffa712017-12-01 19:49:45 -0800336 @Override
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400337 public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
338 }
339
fahadnaeemkhan02ffa712017-12-01 19:49:45 -0800340 @Override
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400341 public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
342 }
343
fahadnaeemkhan02ffa712017-12-01 19:49:45 -0800344 @Override
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400345 public X509Certificate[] getAcceptedIssuers() {
346 return new X509Certificate[0];
347 }
348 } }, new java.security.SecureRandom());
349 } catch (NoSuchAlgorithmException | KeyManagementException e) {
Ray Milkeyba547f92018-02-01 15:22:31 -0800350 throw new IllegalStateException(e);
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400351 }
352
353 return ClientBuilder.newBuilder().sslContext(sslcontext).hostnameVerifier((s1, s2) -> true).build();
354 }
Matteo Gerola7e180c22017-03-30 11:57:58 +0200355
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400356}