/*
* Copyright (c) 2013 David Boissier
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.codinjutsu.tools.jenkins.security;
import com.intellij.openapi.vfs.VirtualFile;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.multipart.*;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.codinjutsu.tools.jenkins.exception.ConfigurationException;
import org.codinjutsu.tools.jenkins.model.VirtualFilePartSource;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class DefaultSecurityClient implements SecurityClient {
private static final String BAD_CRUMB_DATA = "No valid crumb was included in the request";
static final String CRUMB_NAME = ".crumb";
private static final int DEFAULT_SOCKET_TIMEOUT = 10000;
private static final int DEFAULT_CONNECTION_TIMEOUT = 10000;
String crumbData;
protected final HttpClient httpClient;
protected Map<String, VirtualFile> files = new HashMap<String, VirtualFile>();
DefaultSecurityClient(String crumbData) {
this.httpClient = new HttpClient(new MultiThreadedHttpConnectionManager());
this.crumbData = crumbData;
}
@Override
public void connect(URL jenkinsUrl) {
execute(jenkinsUrl);
}
public String execute(URL url) {
String urlStr = url.toString();
ResponseCollector responseCollector = new ResponseCollector();
runMethod(urlStr, responseCollector);
if (isRedirection(responseCollector.statusCode)) {
runMethod(responseCollector.data, responseCollector);
}
return responseCollector.data;
}
@Override
public void setFiles(Map<String, VirtualFile> files) {
this.files = files;
}
private PostMethod addFiles(PostMethod post) {
if (files.size() > 0) {
ArrayList<Part> parts = new ArrayList<Part>();
int i = 0;
for(String key: files.keySet()) {
VirtualFile virtualFile = files.get(key);
parts.add(new StringPart("name", key));
parts.add(new StringPart("json", "{\"parameter\":{\"name\":\"" + key + "\",\"file\":\""+ String.format("file%d", i) +"\"}}"));
parts.add(new FilePart(String.format("file%d", i), new VirtualFilePartSource(virtualFile)));
i++;
}
post.setRequestEntity(new MultipartRequestEntity(parts.toArray(new Part[parts.size()]), post.getParams()));
files.clear();
}
return post;
}
private void runMethod(String url, ResponseCollector responseCollector) {
PostMethod post = new PostMethod(url);
if (isCrumbDataSet()) {
post.addRequestHeader(CRUMB_NAME, crumbData);
}
post = addFiles(post);
InputStream inputStream = null;
try {
if (files.isEmpty()) {
httpClient.getParams().setParameter("http.socket.timeout", DEFAULT_SOCKET_TIMEOUT);
httpClient.getParams().setParameter("http.connection.timeout", DEFAULT_CONNECTION_TIMEOUT);
} else {
httpClient.getParams().setParameter("http.socket.timeout", 0);
httpClient.getParams().setParameter("http.connection.timeout", 0);
}
int statusCode = httpClient.executeMethod(post);
inputStream = post.getResponseBodyAsStream();
String responseBody = IOUtils.toString(inputStream, post.getResponseCharSet());
checkResponse(statusCode, responseBody);
if (HttpURLConnection.HTTP_OK == statusCode) {
responseCollector.collect(statusCode, responseBody);
}
if (isRedirection(statusCode)) {
responseCollector.collect(statusCode, post.getResponseHeader("Location").getValue());
}
} catch (HttpException httpEx) {
throw new ConfigurationException(String.format("HTTP Error during method execution '%s': %s", url, httpEx.getMessage()), httpEx);
} catch (UnknownHostException uhEx) {
throw new ConfigurationException(String.format("Unknown server: %s", uhEx.getMessage()), uhEx);
} catch (IOException ioEx) {
throw new ConfigurationException(String.format("IO Error during method execution '%s': %s", url, ioEx.getMessage()), ioEx);
} finally {
IOUtils.closeQuietly(inputStream);
post.releaseConnection();
}
}
protected void checkResponse(int statusCode, String responseBody) throws AuthenticationException {
if (statusCode == HttpURLConnection.HTTP_NOT_FOUND) {
throw new AuthenticationException("Not found");
}
if (statusCode == HttpURLConnection.HTTP_FORBIDDEN || statusCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
if (StringUtils.containsIgnoreCase(responseBody, BAD_CRUMB_DATA)) {
throw new AuthenticationException("CSRF enabled -> Missing or bad crumb data");
}
throw new AuthenticationException("Unauthorized -> Missing or bad credentials", responseBody);
}
if (HttpURLConnection.HTTP_INTERNAL_ERROR == statusCode) {
throw new AuthenticationException("Server Internal Error: Server unavailable");
}
}
private boolean isRedirection(int statusCode) {
return statusCode / 100 == 3;
}
protected boolean isCrumbDataSet() {
return StringUtils.isNotBlank(crumbData);
}
private static class ResponseCollector {
private int statusCode;
private String data;
void collect(int statusCode, String body) {
this.statusCode = statusCode;
this.data = body;
}
}
}