/* * Copyright 2013-2014 Odysseus Software GmbH * * 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.musicmount.io.server.dav; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.attribute.BasicFileAttributes; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.HttpResponseException; import org.apache.http.client.ResponseHandler; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.config.Registry; import org.apache.http.config.RegistryBuilder; import org.apache.http.conn.socket.ConnectionSocketFactory; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.util.EntityUtils; import org.musicmount.io.server.ServerFileAttributes; import org.musicmount.io.server.ServerFileSystem; import org.musicmount.io.server.ServerPath; import org.musicmount.io.server.ServerResourceProvider; import org.musicmount.util.PositionInputStream; import com.github.sardine.DavResource; import com.github.sardine.Sardine; import com.github.sardine.impl.SardineImpl; import com.github.sardine.impl.handler.VoidResponseHandler; import com.github.sardine.impl.io.ContentLengthInputStream; public class DAVResourceProvider extends ServerResourceProvider { /* * Sardine is NOT thread-safe (because of HTTPContext) */ private final ThreadLocal<Sardine> sardine = new ThreadLocal<Sardine>() { protected Sardine initialValue() { return createSardine(fileSystem); } }; private final ServerFileSystem fileSystem; public DAVResourceProvider(URI serverUri) { this(new ServerFileSystem(serverUri)); } public DAVResourceProvider(URI serverUri, String userInfo) { this(new ServerFileSystem(serverUri, userInfo)); } public DAVResourceProvider(String scheme, String authority, String path) throws URISyntaxException { this(new ServerFileSystem(scheme, authority, path)); } public DAVResourceProvider(String scheme, String host, int port, String path, String user, String password) throws URISyntaxException { this(new ServerFileSystem(scheme, host, port, path, user, password)); } protected DAVResourceProvider(ServerFileSystem fileSystem) { super(fileSystem); if (!"http".equals(fileSystem.getScheme()) && !"https".equals(fileSystem.getScheme())) { throw new IllegalArgumentException("Scheme must be \"http\" or \"https\""); } this.fileSystem = fileSystem; } protected Sardine createSardine(final ServerFileSystem fileSystem) { /* * extract user/password */ String user = null; String password = null; if (fileSystem.getUserInfo() != null) { String[] userAndPassword = fileSystem.getUserInfo().split(":"); user = userAndPassword[0]; password = userAndPassword.length > 1 ? userAndPassword[1] : null; } /* * create customized sardine */ return new SardineImpl(user, password, null) { @Override protected Registry<ConnectionSocketFactory> createDefaultSchemeRegistry() { ConnectionSocketFactory socketFactory; if ("https".equalsIgnoreCase(fileSystem.getScheme())) { socketFactory = createDefaultSecureSocketFactory(); } else { socketFactory = createDefaultSocketFactory(); } return RegistryBuilder.<ConnectionSocketFactory>create() .register(fileSystem.getScheme(), socketFactory) .build(); } @Override protected ConnectionSocketFactory createDefaultSecureSocketFactory() { try { // trust anybody... SSLContext context = SSLContext.getInstance("TLS"); X509TrustManager trustManager = new X509TrustManager() { public void checkClientTrusted(X509Certificate[] xcs, String string) throws CertificateException {} public void checkServerTrusted(X509Certificate[] xcs, String string) throws CertificateException {} public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } }; context.init(null, new TrustManager[]{ trustManager }, null); return new SSLConnectionSocketFactory(context, SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); } catch (NoSuchAlgorithmException | KeyManagementException e) { // should not happen... } return super.createDefaultSecureSocketFactory(); } @Override protected <T> T execute(HttpRequestBase request, ResponseHandler<T> responseHandler) throws IOException { /* * Sardine re-executes a PUT request after a org.apache.http.NoHttpResponseException without resetting it... */ if (request.isAborted()) { request.reset(); } return super.execute(request, responseHandler); } @Override public ContentLengthInputStream get(String url, Map<String, String> headers) throws IOException { /* * abort rather than consume entity for better performance */ final HttpGet get = new HttpGet(url); for (String header : headers.keySet()) { get.addHeader(header, headers.get(header)); } // Must use #execute without handler, otherwise the entity is consumed already after the handler exits. final HttpResponse response = this.execute(get); VoidResponseHandler handler = new VoidResponseHandler(); try { handler.handleResponse(response); // Will consume or abort the entity when the stream is closed. PositionInputStream positionInputStream = new PositionInputStream(response.getEntity().getContent()) { public void close() throws IOException { if (getPosition() == response.getEntity().getContentLength()) { EntityUtils.consume(response.getEntity()); } else { // partial read or unknown content length get.abort(); } } }; return new ContentLengthInputStream(positionInputStream, response.getEntity().getContentLength()); } catch (IOException ex) { get.abort(); throw ex; } } }; } protected Sardine getSardine() { return sardine.get(); } @Override protected BasicFileAttributes getFileAttributes(ServerPath path) throws IOException { List<DavResource> list = getSardine().list(path.toUri().toString(), 0); if (list.size() != 1) { throw new IOException("Could not get file attributes for path: " + path); } return new DAVFileAttributes(list.get(0)); } @Override protected List<ServerFileAttributes> getChildrenAttributes(ServerPath folder) throws IOException { List<DavResource> resources = getSardine().list(folder.toUri().toString(), 1); /* * Older lighttpd servers seems NOT to include the parent as first element! * We therefore check if the first resource matches the parent folder. */ if (resources.isEmpty()) { return Collections.emptyList(); } int start = 1; // collection includes parent folder if (resources.get(0).isDirectory()) { if (fileSystem.getPath(resources.get(0).getPath()).toRealPath().getNameCount() == folder.toRealPath().getNameCount() + 1) { start = 0; } } else { start = 0; } List<ServerFileAttributes> attributes = new ArrayList<>(resources.size() - start); for (int i = start; i < resources.size(); i++) { attributes.add(new DAVFileAttributes(resources.get(i))); } return attributes; } @Override protected boolean exists(ServerPath path) throws IOException { if (path.isDirectory()) { try { return getFileAttributes(path).isDirectory(); } catch (HttpResponseException e) { if (e.getStatusCode() == HttpStatus.SC_NOT_FOUND) { return false; } else { throw new IOException(e.getMessage() + " (" + e.getStatusCode() + ")", e); } } } else { // HEAD, doesn't work for directories return getSardine().exists(path.toUri().toString()); } } @Override protected void delete(ServerPath path) throws IOException { getSardine().delete(path.toUri().toString()); } @Override protected void createDirectory(ServerPath path) throws IOException { getSardine().createDirectory(path.toUri().toString()); } @Override protected InputStream getInputStream(ServerPath path) throws IOException { return getSardine().get(path.toUri().toString()); } @Override protected OutputStream getOutputStream(final ServerPath path) throws IOException { return new ByteArrayOutputStream() { @Override public void close() throws IOException { getSardine().put(path.toUri().toString(), toByteArray()); } }; } }