001/** 002 * Copyright (C) 2006-2020 Talend Inc. - www.talend.com 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.talend.sdk.component.server.front; 017 018import static java.util.Collections.emptyMap; 019import static java.util.Optional.ofNullable; 020import static java.util.concurrent.CompletableFuture.completedFuture; 021import static java.util.stream.Collectors.joining; 022import static java.util.stream.Collectors.toList; 023import static java.util.stream.Collectors.toSet; 024 025import java.io.ByteArrayInputStream; 026import java.io.ByteArrayOutputStream; 027import java.io.IOException; 028import java.io.InputStream; 029import java.nio.charset.StandardCharsets; 030import java.security.Principal; 031import java.util.Collection; 032import java.util.Collections; 033import java.util.List; 034import java.util.Map; 035import java.util.concurrent.CompletableFuture; 036import java.util.concurrent.CompletionStage; 037import java.util.concurrent.ExecutionException; 038import java.util.stream.Stream; 039 040import javax.annotation.PostConstruct; 041import javax.enterprise.context.ApplicationScoped; 042import javax.inject.Inject; 043import javax.json.bind.Jsonb; 044import javax.servlet.ServletContext; 045import javax.servlet.ServletException; 046import javax.servlet.http.HttpServletRequest; 047import javax.ws.rs.HttpMethod; 048import javax.ws.rs.WebApplicationException; 049import javax.ws.rs.core.Context; 050import javax.ws.rs.core.Response; 051import javax.ws.rs.core.UriInfo; 052 053import org.apache.cxf.Bus; 054import org.apache.cxf.transport.http.DestinationRegistry; 055import org.apache.cxf.transport.servlet.ServletController; 056import org.apache.cxf.transport.servlet.servicelist.ServiceListGeneratorServlet; 057import org.talend.sdk.component.server.api.BulkReadResource; 058import org.talend.sdk.component.server.front.cxf.CxfExtractor; 059import org.talend.sdk.component.server.front.memory.InMemoryRequest; 060import org.talend.sdk.component.server.front.memory.InMemoryResponse; 061import org.talend.sdk.component.server.front.memory.MemoryInputStream; 062import org.talend.sdk.component.server.front.memory.SimpleServletConfig; 063import org.talend.sdk.component.server.front.model.BulkRequests; 064import org.talend.sdk.component.server.front.model.BulkResponses; 065import org.talend.sdk.component.server.front.model.ErrorDictionary; 066import org.talend.sdk.component.server.front.model.error.ErrorPayload; 067import org.talend.sdk.component.server.front.security.web.EndpointSecurityService; 068import org.talend.sdk.component.server.service.qualifier.ComponentServer; 069 070import lombok.extern.slf4j.Slf4j; 071 072@Slf4j 073@ApplicationScoped 074public class BulkReadResourceImpl implements BulkReadResource { 075 076 private static final CompletableFuture[] EMPTY_PROMISES = new CompletableFuture[0]; 077 078 @Inject 079 private CxfExtractor cxf; 080 081 @Inject 082 private Bus bus; 083 084 @Inject 085 @Context 086 private ServletContext servletContext; 087 088 @Inject 089 @Context 090 private HttpServletRequest httpServletRequest; 091 092 @Inject 093 @Context 094 private UriInfo uriInfo; 095 096 @Inject 097 @Context 098 private HttpServletRequest request; 099 100 @Inject 101 @ComponentServer 102 private Jsonb defaultMapper; 103 104 @Inject 105 private EndpointSecurityService endpointSecurityService; 106 107 private ServletController controller; 108 109 private final String appPrefix = "/api/v1"; 110 111 private final Collection<String> blacklisted = 112 Stream.of(appPrefix + "/component/icon/", appPrefix + "/component/dependency/").collect(toSet()); 113 114 private final BulkResponses.Result forbiddenInBulkModeResponse = 115 new BulkResponses.Result(Response.Status.FORBIDDEN.getStatusCode(), emptyMap(), 116 "{\"code\":\"UNAUTHORIZED\",\"description\":\"Forbidden endpoint in bulk mode.\"}" 117 .getBytes(StandardCharsets.UTF_8)); 118 119 private final BulkResponses.Result forbiddenResponse = 120 new BulkResponses.Result(Response.Status.FORBIDDEN.getStatusCode(), emptyMap(), 121 "{\"code\":\"UNAUTHORIZED\",\"description\":\"Secured endpoint, ensure to pass the right token.\"}" 122 .getBytes(StandardCharsets.UTF_8)); 123 124 private final BulkResponses.Result invalidResponse = 125 new BulkResponses.Result(Response.Status.BAD_REQUEST.getStatusCode(), emptyMap(), 126 "{\"code\":\"UNEXPECTED\",\"description\":\"unknownEndpoint.\"}".getBytes(StandardCharsets.UTF_8)); 127 128 @PostConstruct 129 private void init() { 130 final DestinationRegistry registry = cxf.getRegistry(); 131 controller = new ServletController(registry, 132 new SimpleServletConfig(servletContext, "Talend Component Kit Bulk Transport"), 133 new ServiceListGeneratorServlet(registry, bus)); 134 } 135 136 @Override 137 public CompletionStage<BulkResponses> bulk(final BulkRequests requests) { 138 final Collection<CompletableFuture<BulkResponses.Result>> responses = 139 ofNullable(requests.getRequests()).map(Collection::stream).orElseGet(Stream::empty).map(request -> { 140 if (isBlacklisted(request)) { 141 return completedFuture(forbiddenInBulkModeResponse); 142 } 143 if ("/api/v1/environment".equals(request.getPath()) 144 && !endpointSecurityService.isAllowed(httpServletRequest)) { 145 return completedFuture(forbiddenResponse); 146 } 147 if (request.getPath() == null || !request.getPath().startsWith(appPrefix) 148 || request.getPath().contains("?")) { 149 return completedFuture(invalidResponse); 150 } 151 return doExecute(request, uriInfo); 152 }).collect(toList()); 153 return CompletableFuture 154 .allOf(responses.toArray(EMPTY_PROMISES)) 155 .handle((ignored, error) -> new BulkResponses(responses.stream().map(it -> { 156 try { 157 return it.get(); 158 } catch (final InterruptedException e) { 159 Thread.currentThread().interrupt(); 160 throw new IllegalStateException(e); 161 } catch (final ExecutionException e) { 162 throw new WebApplicationException(Response 163 .serverError() 164 .entity(new ErrorPayload(ErrorDictionary.UNEXPECTED, e.getMessage())) 165 .build()); 166 } 167 }).collect(toList()))); 168 } 169 170 private boolean isBlacklisted(final BulkRequests.Request request) { 171 return blacklisted.stream().anyMatch(it -> request.getPath() == null || request.getPath().startsWith(it)); 172 } 173 174 private CompletableFuture<BulkResponses.Result> doExecute(final BulkRequests.Request inputRequest, 175 final UriInfo info) { 176 final Map<String, List<String>> headers = 177 ofNullable(inputRequest.getHeaders()).orElseGet(Collections::emptyMap); 178 final String path = ofNullable(inputRequest.getPath()).map(it -> it.substring(appPrefix.length())).orElse("/"); 179 180 // theorically we should encode these params but should be ok this way for now - due to the param we can accept 181 final String queryString = ofNullable(inputRequest.getQueryParameters()) 182 .map(Map::entrySet) 183 .map(Collection::stream) 184 .orElseGet(Stream::empty) 185 .flatMap(it -> ofNullable(it.getValue()) 186 .map(Collection::stream) 187 .orElseGet(Stream::empty) 188 .map(value -> it.getKey() + '=' + value)) 189 .collect(joining("&")); 190 191 final int port = info.getBaseUri().getPort(); 192 final Principal userPrincipal = request.getUserPrincipal(); // this is ap proxy so ready it early 193 final InMemoryRequest request = new InMemoryRequest(ofNullable(inputRequest.getVerb()).orElse(HttpMethod.GET), 194 headers, path, appPrefix + path, appPrefix, queryString, port < 0 ? 8080 : port, servletContext, 195 new MemoryInputStream(ofNullable(inputRequest.getPayload()) 196 .map(it -> it.getBytes(StandardCharsets.UTF_8)) 197 .map(ByteArrayInputStream::new) 198 .map(InputStream.class::cast) 199 .orElse(null)), 200 () -> userPrincipal, controller); 201 final BulkResponses.Result result = new BulkResponses.Result(); 202 final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 203 final CompletableFuture<BulkResponses.Result> promise = new CompletableFuture<>(); 204 final InMemoryResponse response = new InMemoryResponse(() -> true, () -> { 205 result.setResponse(outputStream.toByteArray()); 206 promise.complete(result); 207 }, bytes -> { 208 try { 209 outputStream.write(bytes); 210 } catch (final IOException e) { 211 throw new IllegalStateException(e); 212 } 213 }, (status, responseHeaders) -> { 214 result.setStatus(status); 215 result.setHeaders(headers); 216 return ""; 217 }); 218 request.setResponse(response); 219 try { 220 controller.invoke(request, response); 221 } catch (final ServletException e) { 222 result.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); 223 result 224 .setResponse(defaultMapper 225 .toJson(new ErrorPayload(ErrorDictionary.UNEXPECTED, e.getMessage())) 226 .getBytes(StandardCharsets.UTF_8)); 227 promise.complete(result); 228 throw new IllegalStateException(e); 229 } 230 return promise; 231 } 232}