blob: e4ba243445ed65a460f01dfc43d2f44669ace136 [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;
Georgios Katsikas74a8a442018-06-26 09:23:58 +020040import javax.ws.rs.ProcessingException;
Michal Machbcd58c72017-06-19 17:12:34 +020041import javax.ws.rs.client.Client;
42import javax.ws.rs.client.ClientBuilder;
43import javax.ws.rs.client.Entity;
44import javax.ws.rs.client.WebTarget;
45import javax.ws.rs.core.MediaType;
46import javax.ws.rs.core.Response;
47import javax.ws.rs.core.Response.Status;
Sean Condon5548ce62018-07-30 16:00:10 +010048import javax.ws.rs.sse.InboundSseEvent;
49import javax.ws.rs.sse.SseEventSource;
Michal Machbcd58c72017-06-19 17:12:34 +020050import java.io.ByteArrayInputStream;
51import java.io.IOException;
52import java.io.InputStream;
53import java.nio.charset.StandardCharsets;
54import java.security.KeyManagementException;
55import java.security.KeyStoreException;
56import java.security.NoSuchAlgorithmException;
57import java.security.cert.CertificateException;
58import java.security.cert.X509Certificate;
59import java.util.Base64;
60import java.util.Map;
61import java.util.concurrent.ConcurrentHashMap;
Sean Condon5548ce62018-07-30 16:00:10 +010062import java.util.function.Consumer;
Hesam Rahimi4a409b42016-08-12 18:37:33 -040063
fahadnaeemkhan02ffa712017-12-01 19:49:45 -080064import static com.google.common.base.Preconditions.checkNotNull;
65
Hesam Rahimi4a409b42016-08-12 18:37:33 -040066/**
67 * The implementation of HttpSBController.
68 */
69public class HttpSBControllerImpl implements HttpSBController {
70
Matteo Gerola7e180c22017-03-30 11:57:58 +020071 private static final Logger log = LoggerFactory.getLogger(HttpSBControllerImpl.class);
Hesam Rahimi4a409b42016-08-12 18:37:33 -040072 private static final String XML = "xml";
73 private static final String JSON = "json";
Michele Santuaric372c222017-01-12 09:41:25 +010074 protected static final String DOUBLESLASH = "//";
75 protected static final String COLON = ":";
Hesam Rahimi4a409b42016-08-12 18:37:33 -040076 private static final int STATUS_OK = Response.Status.OK.getStatusCode();
77 private static final int STATUS_CREATED = Response.Status.CREATED.getStatusCode();
78 private static final int STATUS_ACCEPTED = Response.Status.ACCEPTED.getStatusCode();
79 private static final String HTTPS = "https";
80 private static final String AUTHORIZATION_PROPERTY = "authorization";
81 private static final String BASIC_AUTH_PREFIX = "Basic ";
fahadnaeemkhan2675a272017-12-13 13:17:23 -080082 private static final String OAUTH2_BEARER_AUTH_PREFIX = "Bearer ";
Hesam Rahimi4a409b42016-08-12 18:37:33 -040083
84 private final Map<DeviceId, RestSBDevice> deviceMap = new ConcurrentHashMap<>();
85 private final Map<DeviceId, Client> clientMap = new ConcurrentHashMap<>();
Sean Condon5548ce62018-07-30 16:00:10 +010086 private final Map<DeviceId, SseEventSource> sseEventSourceMap = new ConcurrentHashMap<>();
Hesam Rahimi4a409b42016-08-12 18:37:33 -040087
88 public Map<DeviceId, RestSBDevice> getDeviceMap() {
89 return deviceMap;
90 }
91
92 public Map<DeviceId, Client> getClientMap() {
93 return clientMap;
94 }
95
Sean Condon5548ce62018-07-30 16:00:10 +010096 public Map<DeviceId, SseEventSource> getSseEventSourceMap() {
97 return sseEventSourceMap;
98 }
99
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400100 @Override
101 public Map<DeviceId, RestSBDevice> getDevices() {
102 return ImmutableMap.copyOf(deviceMap);
103 }
104
105 @Override
106 public RestSBDevice getDevice(DeviceId deviceInfo) {
107 return deviceMap.get(deviceInfo);
108 }
109
110 @Override
111 public RestSBDevice getDevice(IpAddress ip, int port) {
Matteo Gerola7e180c22017-03-30 11:57:58 +0200112 return deviceMap.values().stream().filter(v -> v.ip().equals(ip) && v.port() == port).findFirst().get();
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400113 }
114
115 @Override
116 public void addDevice(RestSBDevice device) {
117 if (!deviceMap.containsKey(device.deviceId())) {
118 Client client = ignoreSslClient();
fahadnaeemkhan02ffa712017-12-01 19:49:45 -0800119 authenticate(client, device);
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400120 clientMap.put(device.deviceId(), client);
121 deviceMap.put(device.deviceId(), device);
122 } else {
123 log.warn("Trying to add a device that is already existing {}", device.deviceId());
124 }
125
126 }
127
128 @Override
129 public void removeDevice(DeviceId deviceId) {
130 clientMap.remove(deviceId);
131 deviceMap.remove(deviceId);
Sean Condon5548ce62018-07-30 16:00:10 +0100132 sseEventSourceMap.remove(deviceId);
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400133 }
134
135 @Override
Matteo Gerola7e180c22017-03-30 11:57:58 +0200136 public int post(DeviceId device, String request, InputStream payload, MediaType mediaType) {
137 Response response = getResponse(device, request, payload, mediaType);
138 if (response == null) {
139 return Status.NO_CONTENT.getStatusCode();
140 }
141 return response.getStatus();
142 }
143
144 @Override
Matteo Gerola7e180c22017-03-30 11:57:58 +0200145 public <T> T post(DeviceId device, String request, InputStream payload, MediaType mediaType,
146 Class<T> responseClass) {
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400147 Response response = getResponse(device, request, payload, mediaType);
Palash Kalada4798d2017-05-23 20:16:55 +0900148 if (response != null && response.hasEntity()) {
Hesam Rahimi96305542017-06-07 13:59:48 -0400149 // Do not read the entity if the responseClass is of type Response. This would allow the
150 // caller to receive the Response directly and try to read its appropriate entity locally.
151 return responseClass == Response.class ? (T) response : response.readEntity(responseClass);
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400152 }
153 log.error("Response from device {} for request {} contains no entity", device, request);
154 return null;
155 }
156
Matteo Gerola7e180c22017-03-30 11:57:58 +0200157 private Response getResponse(DeviceId device, String request, InputStream payload, MediaType mediaType) {
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400158
159 WebTarget wt = getWebTarget(device, request);
160
161 Response response = null;
162 if (payload != null) {
163 try {
Eunjin Choi51244d32017-05-15 14:09:56 +0900164 response = wt.request(mediaType)
165 .post(Entity.entity(IOUtils.toString(payload, StandardCharsets.UTF_8), mediaType));
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400166 } catch (IOException e) {
Matteo Gerola7e180c22017-03-30 11:57:58 +0200167 log.error("Cannot do POST {} request on device {} because can't read payload", request, device);
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400168 }
169 } else {
Georgios Katsikas186b9582017-05-31 17:25:54 +0200170 response = wt.request(mediaType).post(Entity.entity(null, mediaType));
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400171 }
172 return response;
173 }
174
175 @Override
Matteo Gerola7e180c22017-03-30 11:57:58 +0200176 public int put(DeviceId device, String request, InputStream payload, MediaType mediaType) {
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400177
178 WebTarget wt = getWebTarget(device, request);
179
180 Response response = null;
181 if (payload != null) {
182 try {
Eunjin Choi51244d32017-05-15 14:09:56 +0900183 response = wt.request(mediaType).put(Entity.entity(IOUtils.
184 toString(payload, StandardCharsets.UTF_8), mediaType));
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400185 } catch (IOException e) {
Matteo Gerola7e180c22017-03-30 11:57:58 +0200186 log.error("Cannot do PUT {} request on device {} because can't read payload", request, device);
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400187 }
188 } else {
Eunjin Choi51244d32017-05-15 14:09:56 +0900189 response = wt.request(mediaType).put(Entity.entity(null, mediaType));
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400190 }
Matteo Gerola7e180c22017-03-30 11:57:58 +0200191
192 if (response == null) {
193 return Status.NO_CONTENT.getStatusCode();
194 }
195 return response.getStatus();
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400196 }
197
198 @Override
Matteo Gerola7e180c22017-03-30 11:57:58 +0200199 public InputStream get(DeviceId device, String request, MediaType mediaType) {
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400200 WebTarget wt = getWebTarget(device, request);
201
Eunjin Choi51244d32017-05-15 14:09:56 +0900202 Response s = wt.request(mediaType).get();
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400203
204 if (checkReply(s)) {
Matteo Gerola7e180c22017-03-30 11:57:58 +0200205 return new ByteArrayInputStream(s.readEntity((String.class)).getBytes(StandardCharsets.UTF_8));
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400206 }
207 return null;
208 }
209
210 @Override
Matteo Gerola7e180c22017-03-30 11:57:58 +0200211 public int patch(DeviceId device, String request, InputStream payload, MediaType mediaType) {
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400212
213 try {
214 log.debug("Url request {} ", getUrlString(device, request));
215 HttpPatch httprequest = new HttpPatch(getUrlString(device, request));
fahadnaeemkhan2675a272017-12-13 13:17:23 -0800216 if (deviceMap.get(device).authentication() == AuthenticationScheme.BASIC) {
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400217 String pwd = deviceMap.get(device).password() == null ? "" : COLON + deviceMap.get(device).password();
218 String userPassword = deviceMap.get(device).username() + pwd;
219 String base64string = Base64.getEncoder().encodeToString(userPassword.getBytes(StandardCharsets.UTF_8));
220 httprequest.addHeader(AUTHORIZATION_PROPERTY, BASIC_AUTH_PREFIX + base64string);
fahadnaeemkhan2675a272017-12-13 13:17:23 -0800221 } else if (deviceMap.get(device).authentication() == AuthenticationScheme.OAUTH2) {
222 String token = deviceMap.get(device).token();
223 // TODO: support token types other then bearer of OAuth2 authentication
224 httprequest.addHeader(AUTHORIZATION_PROPERTY, OAUTH2_BEARER_AUTH_PREFIX + token);
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400225 }
226 if (payload != null) {
227 StringEntity input = new StringEntity(IOUtils.toString(payload, StandardCharsets.UTF_8));
Eunjin Choi51244d32017-05-15 14:09:56 +0900228 input.setContentType(mediaType.toString());
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400229 httprequest.setEntity(input);
230 }
231 CloseableHttpClient httpClient;
232 if (deviceMap.containsKey(device) && deviceMap.get(device).protocol().equals(HTTPS)) {
233 httpClient = getApacheSslBypassClient();
234 } else {
235 httpClient = HttpClients.createDefault();
236 }
Matteo Gerola7e180c22017-03-30 11:57:58 +0200237 return httpClient.execute(httprequest).getStatusLine().getStatusCode();
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400238 } catch (IOException | NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) {
Matteo Gerola7e180c22017-03-30 11:57:58 +0200239 log.error("Cannot do PATCH {} request on device {}", request, device, e);
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400240 }
Matteo Gerola7e180c22017-03-30 11:57:58 +0200241 return Status.BAD_REQUEST.getStatusCode();
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400242 }
243
244 @Override
Matteo Gerola7e180c22017-03-30 11:57:58 +0200245 public int delete(DeviceId device, String request, InputStream payload, MediaType mediaType) {
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400246
247 WebTarget wt = getWebTarget(device, request);
248
Matteo Gerola7e180c22017-03-30 11:57:58 +0200249 // FIXME: do we need to delete an entry by enclosing data in DELETE
250 // request?
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400251 // wouldn't it be nice to use PUT to implement the similar concept?
Georgios Katsikas74a8a442018-06-26 09:23:58 +0200252 Response response = null;
253 try {
254 response = wt.request(mediaType).delete();
255 } catch (ProcessingException procEx) {
256 log.error("Cannot issue DELETE {} request on device {}", request, device);
257 return Status.SERVICE_UNAVAILABLE.getStatusCode();
258 }
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400259
Matteo Gerola7e180c22017-03-30 11:57:58 +0200260 return response.getStatus();
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400261 }
262
Sean Condon5548ce62018-07-30 16:00:10 +0100263 @Override
264 public int getServerSentEvents(DeviceId deviceId, String request,
265 Consumer<InboundSseEvent> onEvent,
266 Consumer<Throwable> onError) {
267 if (deviceId == null) {
268 log.warn("Device ID is null", request);
269 return Status.PRECONDITION_FAILED.getStatusCode();
270 }
271
272 if (request == null || request.isEmpty()) {
273 log.warn("Request cannot be empty", request);
274 return Status.PRECONDITION_FAILED.getStatusCode();
275 }
276
277 if (sseEventSourceMap.containsKey(deviceId)) {
278 log.warn("Device", deviceId, "is already listening to an SSE stream");
279 return Status.CONFLICT.getStatusCode();
280 }
281
282 WebTarget wt = getWebTarget(deviceId, request);
283 SseEventSource sseEventSource = SseEventSource.target(wt).build();
284 sseEventSource.register(onEvent, onError);
285 sseEventSource.open();
286 if (sseEventSource.isOpen()) {
287 sseEventSourceMap.put(deviceId, sseEventSource);
288 log.info("Opened Server Sent Events request to ", request, "on", deviceId);
289 while (sseEventSource.isOpen()) {
290 try {
291 Thread.sleep(1010);
292 System.out.println("Listening for SSEs");
293 } catch (InterruptedException e) {
294 log.error("Error", e);
295 }
296 }
297 return Status.NO_CONTENT.getStatusCode();
298 } else {
299 log.error("Unable to open Server Sent Events request to ", request, "to", deviceId);
300 return Status.INTERNAL_SERVER_ERROR.getStatusCode();
301 }
302 }
303
304 @Override
305 public int cancelServerSentEvents(DeviceId deviceId) {
306 if (sseEventSourceMap.containsKey(deviceId)) {
307 sseEventSourceMap.get(deviceId).close();
308 sseEventSourceMap.remove(deviceId);
309 return Status.OK.getStatusCode();
310 } else {
311 return Status.NOT_FOUND.getStatusCode();
312 }
313 }
314
Matteo Gerola7e180c22017-03-30 11:57:58 +0200315 private MediaType typeOfMediaType(String type) {
316 switch (type) {
317 case XML:
318 return MediaType.APPLICATION_XML_TYPE;
319 case JSON:
320 return MediaType.APPLICATION_JSON_TYPE;
Michal Machf0ce45e2017-06-20 11:54:08 +0200321 case MediaType.WILDCARD:
322 return MediaType.WILDCARD_TYPE;
Matteo Gerola7e180c22017-03-30 11:57:58 +0200323 default:
324 throw new IllegalArgumentException("Unsupported media type " + type);
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400325
326 }
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400327 }
328
fahadnaeemkhan02ffa712017-12-01 19:49:45 -0800329 private void authenticate(Client client, RestSBDevice device) {
330 AuthenticationScheme authScheme = device.authentication();
331 if (authScheme == AuthenticationScheme.NO_AUTHENTICATION) {
332 log.debug("{} scheme is specified, ignoring authentication", authScheme);
333 return;
334 } else if (authScheme == AuthenticationScheme.OAUTH2) {
335 String token = checkNotNull(device.token());
336 client.register(OAuth2ClientSupport.feature(token));
337 } else if (authScheme == AuthenticationScheme.BASIC) {
338 String username = device.username();
339 String password = device.password() == null ? "" : device.password();
340 client.register(HttpAuthenticationFeature.basic(username, password));
341 } else {
342 // TODO: Add support for other authentication schemes here.
343 throw new IllegalArgumentException(String.format("Unsupported authentication scheme: %s",
344 authScheme.name()));
345 }
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400346 }
347
348 protected WebTarget getWebTarget(DeviceId device, String request) {
349 log.debug("Sending request to URL {} ", getUrlString(device, request));
350 return clientMap.get(device).target(getUrlString(device, request));
351 }
352
353 //FIXME security issue: this trusts every SSL certificate, even if is self-signed. Also deprecated methods.
354 private CloseableHttpClient getApacheSslBypassClient() throws NoSuchAlgorithmException,
355 KeyManagementException, KeyStoreException {
356 return HttpClients.custom().
357 setHostnameVerifier(new AllowAllHostnameVerifier()).
358 setSslcontext(new SSLContextBuilder()
359 .loadTrustMaterial(null, (arg0, arg1) -> true)
360 .build()).build();
361 }
362
Michal Machbcd58c72017-06-19 17:12:34 +0200363 protected String getUrlString(DeviceId deviceId, String request) {
364 RestSBDevice restSBDevice = deviceMap.get(deviceId);
365 if (restSBDevice == null) {
366 log.warn("restSbDevice cannot be NULL!");
367 return "";
368 }
369 if (restSBDevice.url() != null) {
370 return restSBDevice.protocol() + COLON + DOUBLESLASH + restSBDevice.url() + request;
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400371 } else {
Michal Machbcd58c72017-06-19 17:12:34 +0200372 return restSBDevice.protocol() + COLON + DOUBLESLASH + restSBDevice.ip().toString()
373 + COLON + restSBDevice.port() + request;
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400374 }
375 }
376
377 private boolean checkReply(Response response) {
378 if (response != null) {
Matteo Gerola7e180c22017-03-30 11:57:58 +0200379 boolean statusCode = checkStatusCode(response.getStatus());
380 if (!statusCode && response.hasEntity()) {
381 log.error("Failed request, HTTP error msg : " + response.readEntity(String.class));
382 }
383 return statusCode;
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400384 }
385 log.error("Null reply from device");
386 return false;
387 }
388
389 private boolean checkStatusCode(int statusCode) {
Matteo Gerola7e180c22017-03-30 11:57:58 +0200390 if (statusCode == STATUS_OK || statusCode == STATUS_CREATED || statusCode == STATUS_ACCEPTED) {
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400391 return true;
392 } else {
Matteo Gerola7e180c22017-03-30 11:57:58 +0200393 log.error("Failed request, HTTP error code : " + statusCode);
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400394 return false;
395 }
396 }
397
398 private Client ignoreSslClient() {
399 SSLContext sslcontext = null;
400
401 try {
402 sslcontext = SSLContext.getInstance("TLS");
403 sslcontext.init(null, new TrustManager[]{new X509TrustManager() {
fahadnaeemkhan02ffa712017-12-01 19:49:45 -0800404 @Override
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400405 public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
406 }
407
fahadnaeemkhan02ffa712017-12-01 19:49:45 -0800408 @Override
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400409 public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
410 }
411
fahadnaeemkhan02ffa712017-12-01 19:49:45 -0800412 @Override
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400413 public X509Certificate[] getAcceptedIssuers() {
414 return new X509Certificate[0];
415 }
416 } }, new java.security.SecureRandom());
417 } catch (NoSuchAlgorithmException | KeyManagementException e) {
Ray Milkeyba547f92018-02-01 15:22:31 -0800418 throw new IllegalStateException(e);
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400419 }
420
421 return ClientBuilder.newBuilder().sslContext(sslcontext).hostnameVerifier((s1, s2) -> true).build();
422 }
Matteo Gerola7e180c22017-03-30 11:57:58 +0200423
Hesam Rahimi4a409b42016-08-12 18:37:33 -0400424}