blob: 8bfba0a2cfd4db6712288c58ae714f8fa815e008 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2011-2020 The University of York, Aston University.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License, v. 2.0 are satisfied: GNU General Public License, version 3.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-3.0
*
* Contributors:
* Antonio Garcia-Dominguez - initial API and implementation
******************************************************************************/
package org.eclipse.hawk.http;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Objects;
import org.apache.commons.io.IOUtils;
import org.apache.http.Header;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.utils.DateUtils;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.LaxRedirectStrategy;
import org.eclipse.hawk.core.IConsole;
import org.eclipse.hawk.core.ICredentialsStore;
import org.eclipse.hawk.core.ICredentialsStore.Credentials;
import org.eclipse.hawk.core.IModelIndexer;
import org.eclipse.hawk.core.IVcsManager;
import org.eclipse.hawk.core.VcsChangeType;
import org.eclipse.hawk.core.VcsCommit;
import org.eclipse.hawk.core.VcsCommitItem;
import org.eclipse.hawk.core.VcsRepositoryDelta;
public class HTTPManager implements IVcsManager {
private static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition";
private static final String FIRST_REV = "0";
private boolean isActive;
private IConsole console;
private URI repositoryURI;
private String username;
private String password;
private String lastETag;
/**
* These two fields are used when the server does not provide ETag
* or Last-Modified headers. The class will need to use the digest
* (SHA-1) of the response as the version number.
*
* In order to avoid having to download the content twice, it will
* remember this digested version and reuse it in import() if the
* revision matches the SHA-1.
*/
private String lastDigest;
private byte[] lastDigestedResponse;
/**
* This remembers the last revision we computed getDelta(...) on, to provide a
* simple single-version change delta functionality.
*/
private String lastDelta;
/**
* Not all locations will have the filename embedded in the URL
*
* This field is used to store the extracted filename from the HTTP
* request if available, to ensure files are correctly retrieved when
* creating deltas
*/
private String lastFilename;
private IModelIndexer indexer;
private boolean isFrozen = false;
@Override
public String getCurrentRevision() throws Exception {
try (final CloseableHttpClient cl = createClient()) {
/*
* Since HTTP servers can be quite varied, we try several methods in sequence:
*
* - ETag headers are preferred, since these explicitly check for changes.
*
* - Otherwise, Last-Modified dates are used.
*
* - Otherwise, compute a SHA1 on the response. This response will be reused the
* next time we import, as long as we use that same SHA1 as the revision.
*
* We try first a HEAD request with ETag/Last-Modified, and if that doesn't work
* we use a GET request and use ETag / Last-Modified / SHA1.
*/
final HttpHead headRequest = new HttpHead(repositoryURI);
decorateCurrentRevisionRequest(headRequest);
try (CloseableHttpResponse response = cl.execute(headRequest)) {
String headRevision = getRevision(response, false);
if (headRevision != null) {
return headRevision;
}
}
final HttpGet getRequest = new HttpGet(repositoryURI);
decorateCurrentRevisionRequest(getRequest);
try (CloseableHttpResponse response = cl.execute(getRequest)) {
String getRev = getRevision(response, true);
if (getRev != null) {
return getRev;
}
}
}
// No way to detect changes - just fetch the file once
return "1";
}
protected void decorateCurrentRevisionRequest(final HttpRequestBase request) {
if (lastETag != null) {
request.setHeader(HttpHeaders.ETAG, lastETag);
request.setHeader(HttpHeaders.IF_NONE_MATCH, "*");
}
}
protected String getRevision(final HttpResponse response, boolean useDigest) {
// Extract filename from request
final Header cdHeader = response.getFirstHeader(HEADER_CONTENT_DISPOSITION);
if (cdHeader != null) {
lastFilename = Arrays.stream(cdHeader.getElements())
.map(e -> e.getParameterByName("filename"))
.filter(Objects::nonNull)
.map(NameValuePair::getValue)
.findFirst()
.orElse("");
}
if (response.getStatusLine().getStatusCode() == 304) {
// Not-Modified, as told by the server
return lastETag;
} else if (response.getStatusLine().getStatusCode() != 200) {
// Request failed for some reason (4xx, 5xx: we already
// handle 3xx redirects)
return FIRST_REV;
}
final Header etagHeader = response.getFirstHeader(HttpHeaders.ETAG);
if (etagHeader != null) {
lastETag = etagHeader.getValue();
return lastETag;
}
final Header lmHeader = response.getFirstHeader(HttpHeaders.LAST_MODIFIED);
if (lmHeader != null) {
final Date lmDate = DateUtils.parseDate(lmHeader.getValue());
return lmDate.getTime() + "";
}
if (useDigest) {
try {
final MessageDigest md = MessageDigest.getInstance("SHA");
final ByteArrayOutputStream bos = new ByteArrayOutputStream();
try (InputStream is = response.getEntity().getContent();
DigestInputStream dis = new DigestInputStream(is, md)) {
IOUtils.copy(dis, bos);
}
lastDigestedResponse = bos.toByteArray();
lastDigest = Base64.getEncoder().encodeToString(md.digest());
return lastDigest;
} catch (NoSuchAlgorithmException ex) {
console.printerrln(ex);
} catch (IOException ex) {
console.printerrln(ex);
}
}
return null;
}
protected CloseableHttpClient createClient() {
final HttpClientBuilder builder = HttpClientBuilder.create();
// Provide username and password if specified
if (username != null) {
final BasicCredentialsProvider credProvider = new BasicCredentialsProvider();
credProvider.setCredentials(new AuthScope(new HttpHost(repositoryURI.getHost())),
new UsernamePasswordCredentials(username, password));
builder.setDefaultCredentialsProvider(credProvider);
}
// Follow redirects
builder.setRedirectStrategy(new LaxRedirectStrategy());
return builder.build();
}
@Override
public String getFirstRevision() throws Exception {
return FIRST_REV;
}
@Override
public Collection<VcsCommitItem> getDelta(String endRevision) throws Exception {
return getDelta(FIRST_REV, endRevision).getCompactedCommitItems();
}
@Override
public VcsRepositoryDelta getDelta(String startRevision, String endRevision) throws Exception {
VcsRepositoryDelta delta;
if (lastDelta == null || !lastDelta.equals(endRevision)) {
VcsCommit c = new VcsCommit();
c.setAuthor("Unknown");
c.setJavaDate(new Date());
c.setMessage("HTTP file changed: " + repositoryURI);
c.setRevision(endRevision);
VcsCommitItem ci = new VcsCommitItem();
Path path = Paths.get(repositoryURI.getPath());
if (lastFilename != null && !path.endsWith(lastFilename)) {
path = Paths.get(path.toString(), lastFilename);
}
ci.setPath(path.toString());
ci.setChangeType(VcsChangeType.UPDATED);
ci.setCommit(c);
c.getItems().add(ci);
delta = new VcsRepositoryDelta(Collections.singleton(c));
} else {
delta = new VcsRepositoryDelta(Collections.emptyList());
}
delta.setManager(this);
lastDelta = endRevision;
return delta;
}
@Override
public File importFile(String revision, String path, File temp) {
if (revision != null && revision.equals(lastDigest)) {
// We have the stored response with this SHA1 - reuse it
try (ByteArrayInputStream bis = new ByteArrayInputStream(lastDigestedResponse)) {
Files.copy(bis, temp.toPath());
} catch (IOException e) {
console.printerrln(e);
}
} else {
// We do not have the response already stored in memory - fetch it
try (CloseableHttpClient cl = createClient()) {
try (CloseableHttpResponse response = cl.execute(new HttpGet(repositoryURI))) {
Files.copy(response.getEntity().getContent(), temp.toPath());
return temp;
}
} catch (IOException e) {
console.printerrln(e);
}
}
return null;
}
@Override
public boolean isActive() {
return isActive;
}
@Override
public void init(String vcsloc, IModelIndexer indexer) throws URISyntaxException {
console = indexer.getConsole();
this.repositoryURI = new URI(vcsloc);
this.indexer = indexer;
}
@Override
public void run() throws Exception {
try {
final ICredentialsStore credStore = indexer.getCredentialsStore();
if (username != null) {
// The credentials were provided by a previous setCredentials
// call: retry the change to the credentials store.
setCredentials(username, password, credStore);
} else {
final Credentials credentials = credStore.get(repositoryURI.toString());
if (credentials != null) {
this.username = credentials.getUsername();
this.password = credentials.getPassword();
} else {
/*
* If we use null for the default username/password, SVNKit
* will try to use the GNOME keyring in Linux, and that will
* lock up our Eclipse instance in some cases.
*/
console.printerrln("No username/password recorded for the repository " + repositoryURI);
this.username = "";
this.password = "";
}
}
isActive = true;
} catch (Exception e) {
console.printerrln("exception in svnmanager run():");
console.printerrln(e);
}
}
@Override
public void shutdown() {
repositoryURI = null;
console = null;
}
@Override
public String getLocation() {
return repositoryURI.toString();
}
@Override
public String getUsername() {
return username;
}
@Override
public String getPassword() {
return password;
}
@Override
public void setCredentials(String username, String password, ICredentialsStore credStore) {
if (username != null && password != null && repositoryURI != null
&& (!username.equals(this.username) || !password.equals(this.password))) {
try {
credStore.put(repositoryURI.toString(), new Credentials(username, password));
} catch (Exception e) {
console.printerrln("Could not save new username/password");
console.printerrln(e);
}
}
this.username = username;
this.password = password;
}
@Override
public String getHumanReadableName() {
return "HTTP Monitor";
}
@Override
public boolean isAuthSupported() {
return true;
}
@Override
public boolean isPathLocationAccepted() {
return false;
}
@Override
public boolean isURLLocationAccepted() {
return true;
}
@Override
public String getRepositoryPath(String rawPath) {
final String sRepositoryURI = repositoryURI.toString();
if (rawPath.startsWith(sRepositoryURI)) {
return rawPath.substring(sRepositoryURI.length());
}
return rawPath;
}
@Override
public boolean isFrozen() {
return isFrozen;
}
@Override
public void setFrozen(boolean f) {
isFrozen = f;
}
@Override
public String getDefaultLocation() {
return "http://host:port/path";
}
}