/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.apache.sshd.common.scp; import java.io.Serializable; import java.util.Objects; import org.apache.sshd.common.auth.MutableUserHolder; import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.common.util.OsUtils; import org.apache.sshd.common.util.ValidateUtils; /** * Represents a local or remote SCP location in the format {@code user@host:path} * for a remote path and a simple path for a local one. If user is omitted for a * remote path then current user is used. * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a> */ public class ScpLocation implements MutableUserHolder, Serializable, Cloneable { public static final char HOST_PART_SEPARATOR = ':'; public static final char USERNAME_PART_SEPARATOR = '@'; private static final long serialVersionUID = 5450230457030600136L; private String host; private String username; private String path; public ScpLocation() { this(null); } /** * @param locSpec The location specification - ignored if {@code null}/empty * @see #update(String, ScpLocation) * @throws IllegalArgumentException if invalid specification */ public ScpLocation(String locSpec) { update(locSpec, this); } public String getHost() { return host; } public void setHost(String host) { this.host = host; } public boolean isLocal() { return GenericUtils.isEmpty(getHost()); } @Override public String getUsername() { return username; } @Override public void setUsername(String username) { this.username = username; } /** * Resolves the effective username to use for a remote location. * If username not set then uses the current username * * @return The resolved username * @see #getUsername() * @see OsUtils#getCurrentUser() */ public String resolveUsername() { String user = getUsername(); if (GenericUtils.isEmpty(user)) { return OsUtils.getCurrentUser(); } else { return user; } } public String getPath() { return path; } public void setPath(String path) { this.path = path; } @Override public int hashCode() { return Objects.hash(getHost(), resolveUsername(), OsUtils.getComparablePath(getPath())); } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (this == obj) { return true; } if (getClass() != obj.getClass()) { return false; } ScpLocation other = (ScpLocation) obj; if (this.isLocal() != other.isLocal()) { return false; } String thisPath = OsUtils.getComparablePath(getPath()); String otherPath = OsUtils.getComparablePath(other.getPath()); if (!Objects.equals(thisPath, otherPath)) { return false; } if (isLocal()) { return true; } // we know other is also remote or we would not have reached this point return Objects.equals(resolveUsername(), other.resolveUsername()) && Objects.equals(getHost(), other.getHost()); } @Override public ScpLocation clone() { try { return getClass().cast(super.clone()); } catch (CloneNotSupportedException e) { // unexpected throw new RuntimeException("Failed to clone " + toString(), e); } } @Override public String toString() { String p = getPath(); if (isLocal()) { return p; } return resolveUsername() + String.valueOf(USERNAME_PART_SEPARATOR) + getHost() + String.valueOf(HOST_PART_SEPARATOR) + p; } /** * Parses a local or remote SCP location in the format {@code user@host:path} * * @param locSpec The location specification - ignored if {@code null}/empty * @return The {@link ScpLocation} or {@code null} if no specification provider * @throws IllegalArgumentException if invalid specification * @see #update(String, ScpLocation) */ public static ScpLocation parse(String locSpec) { return GenericUtils.isEmpty(locSpec) ? null : update(locSpec, new ScpLocation()); } /** * Parses a local or remote SCP location in the format {@code user@host:path} * * @param <L> Type of {@link ScpLocation} being updated * @param locSpec The location specification - ignored if {@code null}/empty * @param location The {@link ScpLocation} to update - never {@code null} * @return The updated location (unless no specification) * @throws IllegalArgumentException if invalid specification */ public static <L extends ScpLocation> L update(String locSpec, L location) { Objects.requireNonNull(location, "No location to update"); if (GenericUtils.isEmpty(locSpec)) { return location; } location.setHost(null); location.setUsername(null); int pos = locSpec.indexOf(HOST_PART_SEPARATOR); if (pos < 0) { // assume a local path location.setPath(locSpec); return location; } /* * NOTE !!! in such a case there may be confusion with a host named 'a', * but there is a limit to how smart we can be... */ if ((pos == 1) && OsUtils.isWin32()) { char drive = locSpec.charAt(0); if (((drive >= 'a') && (drive <= 'z')) || ((drive >= 'A') && (drive <= 'Z'))) { location.setPath(locSpec); return location; } } String login = locSpec.substring(0, pos); ValidateUtils.checkTrue(pos < (locSpec.length() - 1), "Invalid remote specification (missing path): %s", locSpec); location.setPath(locSpec.substring(pos + 1)); pos = login.indexOf(USERNAME_PART_SEPARATOR); ValidateUtils.checkTrue(pos != 0, "Invalid remote specification (missing username): %s", locSpec); if (pos < 0) { location.setHost(login); } else { location.setUsername(login.substring(0, pos)); ValidateUtils.checkTrue(pos < (login.length() - 1), "Invalid remote specification (missing host): %s", locSpec); location.setHost(login.substring(pos + 1)); } return location; } }