Merge pull request #1416 from scm-manager/feature/hg_hooks_over_tcp

Feature/hg hooks over tcp
This commit is contained in:
René Pfeuffer
2020-11-27 13:12:07 +01:00
committed by GitHub
75 changed files with 2602 additions and 2982 deletions

View File

@@ -6,11 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Added
- Add tooltips to short links on repository overview ([#1441](https://github.com/scm-manager/scm-manager/pull/1441))
- Show the date of the last commit for branches in the frontend ([#1439](https://github.com/scm-manager/scm-manager/pull/1439))
- Unify and add description to key view across user settings ([#1440](https://github.com/scm-manager/scm-manager/pull/1440))
### Changed
- Send mercurial hook callbacks over separate tcp socket instead of http ([#1416](https://github.com/scm-manager/scm-manager/pull/1416))
### Fixed
- Language detection of files with interpreter parameters e.g.: `#!/usr/bin/make -f` ([#1450](https://github.com/scm-manager/scm-manager/issues/1450))

View File

@@ -506,7 +506,7 @@
<!-- xml -->
<dependency>
<groupId>jakarta.xml.bind</groupId>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>${jaxb.version}</version>
</dependency>

View File

@@ -250,6 +250,12 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>

View File

@@ -0,0 +1,71 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.MDC;
import java.util.Optional;
/**
* Id of the current transaction.
* The transaction id is mainly used for logging and debugging.
*
* @since 2.10.0
*/
public final class TransactionId {
@VisibleForTesting
public static final String KEY = "transaction_id";
private TransactionId() {
}
/**
* Binds the given transaction id to the current thread.
*
* @param transactionId transaction id
*/
public static void set(String transactionId) {
MDC.put(KEY, transactionId);
}
/**
* Returns an optional transaction id.
* If there is no transaction id bound to the thread, the method will return an empty optional.
*
* @return optional transaction id
*/
public static Optional<String> get() {
return Optional.ofNullable(MDC.get(KEY));
}
/**
* Removes a bound transaction id from the current thread.
*/
public static void clear() {
MDC.remove(KEY);
}
}

View File

@@ -0,0 +1,42 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class TransactionIdTest {
@Test
void shouldSetGetAndClear() {
TransactionId.set("42");
assertThat(TransactionId.get()).contains("42");
TransactionId.clear();
assertThat(TransactionId.get()).isEmpty();
}
}

View File

@@ -50,9 +50,23 @@
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>

View File

@@ -30,11 +30,13 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@NoArgsConstructor
@Getter
@Setter
@NoArgsConstructor
@SuppressWarnings("java:S2160") // we don't need equals for dto
public class HgConfigDto extends HalRepresentation implements UpdateHgConfigDto {
private boolean disabled;
private String encoding;
@@ -44,7 +46,6 @@ public class HgConfigDto extends HalRepresentation implements UpdateHgConfigDto
private boolean useOptimizedBytecode;
private boolean showRevisionInId;
private boolean enableHttpPostArgs;
private boolean disableHookSSLValidation;
@Override
@SuppressWarnings("squid:S1185") // We want to have this method available in this package

View File

@@ -39,7 +39,5 @@ interface UpdateHgConfigDto {
boolean isShowRevisionInId();
boolean isDisableHookSSLValidation();
boolean isEnableHttpPostArgs();
}

View File

@@ -1,391 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContext;
import sonia.scm.util.IOUtil;
import sonia.scm.util.Util;
import sonia.scm.web.HgUtil;
import javax.xml.bind.JAXBException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*/
public class AbstractHgHandler
{
/** Field description */
protected static final String ENV_ID_REVISION = "SCM_ID_REVISION";
/** Field description */
protected static final String ENV_NODE = "HG_NODE";
/** Field description */
protected static final String ENV_PAGE_LIMIT = "SCM_PAGE_LIMIT";
/** Field description */
protected static final String ENV_PAGE_START = "SCM_PAGE_START";
/** Field description */
protected static final String ENV_PATH = "SCM_PATH";
/** Field description */
protected static final String ENV_REPOSITORY_PATH = "SCM_REPOSITORY_PATH";
/** Field description */
protected static final String ENV_REVISION = "SCM_REVISION";
/** Field description */
protected static final String ENV_REVISION_END = "SCM_REVISION_END";
/** Field description */
protected static final String ENV_REVISION_START = "SCM_REVISION_START";
/** Field description */
private static final String ENCODING = "UTF-8";
/** mercurial encoding */
private static final String ENV_HGENCODING = "HGENCODING";
/** Field description */
private static final String ENV_PENDING = "HG_PENDING";
/** python encoding */
private static final String ENV_PYTHONIOENCODING = "PYTHONIOENCODING";
/** Field description */
private static final String ENV_PYTHONPATH = "PYTHONPATH";
/**
* the logger for AbstractHgCommand
*/
private static final Logger logger =
LoggerFactory.getLogger(AbstractHgHandler.class);
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*
*
* @param handler
* @param context
* @param repository
*/
protected AbstractHgHandler(HgRepositoryHandler handler, HgContext context,
Repository repository)
{
this(handler, context, repository, handler.getDirectory(repository.getId()));
}
/**
* Constructs ...
*
*
*
* @param handler
* @param context
* @param repository
* @param repositoryDirectory
*/
protected AbstractHgHandler(HgRepositoryHandler handler, HgContext context,
Repository repository, File repositoryDirectory)
{
this.handler = handler;
this.context = context;
this.repository = repository;
this.repositoryDirectory = repositoryDirectory;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param revision
* @param path
*
* @return
*/
protected Map<String, String> createEnvironment(String revision, String path)
{
Map<String, String> env = new HashMap<>();
env.put(ENV_REVISION, HgUtil.getRevision(revision));
env.put(ENV_PATH, Util.nonNull(path));
return env;
}
/**
* Method description
*
*
* @param args
*
* @return
*
* @throws IOException
*/
protected Process createHgProcess(String... args) throws IOException
{
return createHgProcess(new HashMap<String, String>(), args);
}
/**
* Method description
*
*
* @param extraEnv
* @param args
*
* @return
*
* @throws IOException
*/
protected Process createHgProcess(Map<String, String> extraEnv,
String... args)
throws IOException
{
return createProcess(extraEnv, handler.getConfig().getHgBinary(), args);
}
/**
* Method description
*
*
* @param script
* @param extraEnv
*
* @return
*
* @throws IOException
*/
protected Process createScriptProcess(HgPythonScript script,
Map<String, String> extraEnv)
throws IOException
{
return createProcess(extraEnv, handler.getConfig().getPythonBinary(),
script.getFile(SCMContext.getContext()).getAbsolutePath());
}
/**
* Method description
*
*
* @param errorStream
*/
protected void handleErrorStream(final InputStream errorStream)
{
if (errorStream != null)
{
new Thread(new Runnable()
{
@Override
public void run()
{
try
{
String content = IOUtil.getContent(errorStream);
if (Util.isNotEmpty(content))
{
logger.error(content.trim());
}
}
catch (IOException ex)
{
logger.error("error during logging", ex);
}
}
}).start();
}
}
//~--- get methods ----------------------------------------------------------
protected <T> T getResultFromScript(Class<T> resultType, HgPythonScript script) throws IOException {
return getResultFromScript(resultType, script,
new HashMap<String, String>());
}
@SuppressWarnings("unchecked")
protected <T> T getResultFromScript(Class<T> resultType,
HgPythonScript script, Map<String, String> extraEnv)
throws IOException
{
Process p = createScriptProcess(script, extraEnv);
handleErrorStream(p.getErrorStream());
try (InputStream input = p.getInputStream()) {
return (T) handler.getJaxbContext().createUnmarshaller().unmarshal(input);
} catch (JAXBException ex) {
logger.error("could not parse result", ex);
throw new InternalRepositoryException(repository, "could not parse result", ex);
}
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param extraEnv
* @param cmd
* @param args
*
* @return
*
* @throws IOException
*/
private Process createProcess(Map<String, String> extraEnv, String cmd,
String... args)
throws IOException
{
HgConfig config = handler.getConfig();
List<String> cmdList = new ArrayList<String>();
cmdList.add(cmd);
if (Util.isNotEmpty(args))
{
cmdList.addAll(Arrays.asList(args));
}
if (logger.isDebugEnabled())
{
StringBuilder msg = new StringBuilder("create process for [");
Iterator<String> it = cmdList.iterator();
while (it.hasNext())
{
msg.append(it.next());
if (it.hasNext())
{
msg.append(", ");
}
}
msg.append("]");
logger.debug(msg.toString());
}
ProcessBuilder pb = new ProcessBuilder(cmdList);
pb.directory(repositoryDirectory);
Map<String, String> env = pb.environment();
// force utf-8 encoding for mercurial and python
env.put(ENV_PYTHONIOENCODING, ENCODING);
env.put(ENV_HGENCODING, ENCODING);
//J-
env.put(ENV_ID_REVISION,
String.valueOf(handler.getConfig().isShowRevisionInId())
);
//J+
if (context.isSystemEnvironment())
{
env.putAll(System.getenv());
}
if (context.isPending())
{
if (logger.isDebugEnabled())
{
logger.debug("enable hg pending for {}",
repositoryDirectory.getAbsolutePath());
}
env.put(ENV_PENDING, repositoryDirectory.getAbsolutePath());
if (extraEnv.containsKey(ENV_REVISION_START))
{
env.put(ENV_NODE, extraEnv.get(ENV_REVISION_START));
}
}
env.put(ENV_PYTHONPATH, HgUtil.getPythonPath(config));
env.put(ENV_REPOSITORY_PATH, repositoryDirectory.getAbsolutePath());
env.putAll(extraEnv);
if (logger.isTraceEnabled())
{
StringBuilder msg = new StringBuilder("start process in directory '");
msg.append(repositoryDirectory.getAbsolutePath()).append(
"' with env: \n");
for (Map.Entry<String, String> e : env.entrySet())
{
msg.append(" ").append(e.getKey());
msg.append(" = ").append(e.getValue());
msg.append("\n");
}
logger.trace(msg.toString());
}
return pb.start();
}
//~--- fields ---------------------------------------------------------------
/** Field description */
protected Repository repository;
/** Field description */
protected File repositoryDirectory;
/** Field description */
private HgContext context;
/** Field description */
private HgRepositoryHandler handler;
}

View File

@@ -0,0 +1,143 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import sonia.scm.TransactionId;
import sonia.scm.repository.hooks.HookEnvironment;
import sonia.scm.repository.hooks.HookServer;
import sonia.scm.security.AccessToken;
import sonia.scm.security.AccessTokenBuilderFactory;
import sonia.scm.security.CipherUtil;
import sonia.scm.security.Xsrf;
import sonia.scm.web.HgUtil;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.File;
import java.io.IOException;
import java.util.Map;
@Singleton
public class DefaultHgEnvironmentBuilder implements HgEnvironmentBuilder {
@VisibleForTesting
static final String ENV_PYTHON_PATH = "PYTHONPATH";
@VisibleForTesting
static final String ENV_HOOK_PORT = "SCM_HOOK_PORT";
@VisibleForTesting
static final String ENV_CHALLENGE = "SCM_CHALLENGE";
@VisibleForTesting
static final String ENV_BEARER_TOKEN = "SCM_BEARER_TOKEN";
@VisibleForTesting
static final String ENV_REPOSITORY_NAME = "REPO_NAME";
@VisibleForTesting
static final String ENV_REPOSITORY_PATH = "SCM_REPOSITORY_PATH";
@VisibleForTesting
static final String ENV_REPOSITORY_ID = "SCM_REPOSITORY_ID";
@VisibleForTesting
static final String ENV_HTTP_POST_ARGS = "SCM_HTTP_POST_ARGS";
@VisibleForTesting
static final String ENV_TRANSACTION_ID = "SCM_TRANSACTION_ID";
private final AccessTokenBuilderFactory accessTokenBuilderFactory;
private final HgRepositoryHandler repositoryHandler;
private final HookEnvironment hookEnvironment;
private final HookServer server;
private int hookPort = -1;
@Inject
public DefaultHgEnvironmentBuilder(
AccessTokenBuilderFactory accessTokenBuilderFactory, HgRepositoryHandler repositoryHandler,
HookEnvironment hookEnvironment, HookServer server
) {
this.accessTokenBuilderFactory = accessTokenBuilderFactory;
this.repositoryHandler = repositoryHandler;
this.hookEnvironment = hookEnvironment;
this.server = server;
}
@Override
public Map<String, String> read(Repository repository) {
ImmutableMap.Builder<String, String> env = ImmutableMap.builder();
read(env, repository);
return env.build();
}
@Override
public Map<String, String> write(Repository repository) {
ImmutableMap.Builder<String, String> env = ImmutableMap.builder();
read(env, repository);
write(env);
return env.build();
}
private void read(ImmutableMap.Builder<String, String> env, Repository repository) {
HgConfig config = repositoryHandler.getConfig();
env.put(ENV_PYTHON_PATH, HgUtil.getPythonPath(config));
File directory = repositoryHandler.getDirectory(repository.getId());
env.put(ENV_REPOSITORY_NAME, repository.getNamespace() + "/" + repository.getName());
env.put(ENV_REPOSITORY_ID, repository.getId());
env.put(ENV_REPOSITORY_PATH, directory.getAbsolutePath());
// enable experimental httppostargs protocol of mercurial
// Issue 970: https://goo.gl/poascp
env.put(ENV_HTTP_POST_ARGS, String.valueOf(config.isEnableHttpPostArgs()));
}
private void write(ImmutableMap.Builder<String, String> env) {
env.put(ENV_HOOK_PORT, String.valueOf(getHookPort()));
env.put(ENV_BEARER_TOKEN, accessToken());
env.put(ENV_CHALLENGE, hookEnvironment.getChallenge());
TransactionId.get().ifPresent(transactionId -> env.put(ENV_TRANSACTION_ID, transactionId));
}
private String accessToken() {
AccessToken accessToken = accessTokenBuilderFactory.create()
// disable xsrf protection, because we can not access the http servlet request for verification
.custom(Xsrf.TOKEN_KEY, null)
.build();
return CipherUtil.getInstance().encode(accessToken.compact());
}
private synchronized int getHookPort() {
if (hookPort > 0) {
return hookPort;
}
try {
hookPort = server.start();
} catch (IOException ex) {
throw new IllegalStateException("failed to start mercurial hook server");
}
return hookPort;
}
}

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
@@ -36,20 +36,10 @@ import javax.xml.bind.annotation.XmlTransient;
* @author Sebastian Sdorra
*/
@XmlRootElement(name = "config")
public class HgConfig extends RepositoryConfig
{
public class HgConfig extends RepositoryConfig {
public static final String PERMISSION = "hg";
/**
* Constructs ...
*
*/
public HgConfig() {}
//~--- get methods ----------------------------------------------------------
@Override
@XmlTransient // Only for permission checks, don't serialize to XML
public String getId() {
@@ -123,10 +113,6 @@ public class HgConfig extends RepositoryConfig
return useOptimizedBytecode;
}
public boolean isDisableHookSSLValidation() {
return disableHookSSLValidation;
}
public boolean isEnableHttpPostArgs() {
return enableHttpPostArgs;
}
@@ -216,10 +202,6 @@ public class HgConfig extends RepositoryConfig
this.useOptimizedBytecode = useOptimizedBytecode;
}
public void setDisableHookSSLValidation(boolean disableHookSSLValidation) {
this.disableHookSSLValidation = disableHookSSLValidation;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
@@ -242,9 +224,4 @@ public class HgConfig extends RepositoryConfig
private boolean enableHttpPostArgs = false;
/**
* disable validation of ssl certificates for mercurial hook
* @see <a href="https://goo.gl/zH5eY8">Issue 959</a>
*/
private boolean disableHookSSLValidation = false;
}

View File

@@ -1,121 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*/
public class HgContext
{
/**
* Constructs ...
*
*/
public HgContext() {}
/**
* Constructs ...
*
*
* @param pending
*/
public HgContext(boolean pending)
{
this.pending = pending;
}
/**
* Constructs ...
*
*
* @param pending
* @param systemEnvironment
*/
public HgContext(boolean pending, boolean systemEnvironment)
{
this.pending = pending;
this.systemEnvironment = systemEnvironment;
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
public boolean isPending()
{
return pending;
}
/**
* Method description
*
*
* @return
*/
public boolean isSystemEnvironment()
{
return systemEnvironment;
}
//~--- set methods ----------------------------------------------------------
/**
* Method description
*
*
* @param pending
*/
public void setPending(boolean pending)
{
this.pending = pending;
}
/**
* Method description
*
*
* @param systemEnvironment
*/
public void setSystemEnvironment(boolean systemEnvironment)
{
this.systemEnvironment = systemEnvironment;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private boolean pending = false;
/** Field description */
private boolean systemEnvironment = true;
}

View File

@@ -1,96 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.annotations.VisibleForTesting;
import com.google.inject.OutOfScopeException;
import com.google.inject.Provider;
import com.google.inject.ProvisionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
/**
* Injection provider for {@link HgContext}.
* This provider returns an instance {@link HgContext} from request scope, if no {@link HgContext} could be found in
* request scope (mostly because the scope is not available) a new {@link HgContext} gets returned.
*
* @author Sebastian Sdorra
*/
public class HgContextProvider implements Provider<HgContext>
{
/**
* the LOG for HgContextProvider
*/
private static final Logger LOG =
LoggerFactory.getLogger(HgContextProvider.class);
//~--- get methods ----------------------------------------------------------
private Provider<HgContextRequestStore> requestStoreProvider;
@Inject
public HgContextProvider(Provider<HgContextRequestStore> requestStoreProvider) {
this.requestStoreProvider = requestStoreProvider;
}
@VisibleForTesting
public HgContextProvider() {
}
@Override
public HgContext get() {
HgContext context = fetchContextFromRequest();
if (context != null) {
LOG.trace("return HgContext from request store");
return context;
}
LOG.trace("could not find context in request scope, returning new instance");
return new HgContext();
}
private HgContext fetchContextFromRequest() {
try {
if (requestStoreProvider != null) {
return requestStoreProvider.get().get();
} else {
LOG.trace("no request store provider defined, could not return context from request");
return null;
}
} catch (ProvisionException ex) {
if (ex.getCause() instanceof OutOfScopeException) {
LOG.trace("we are currently out of request scope, failed to retrieve context");
return null;
} else {
throw ex;
}
}
}
}

View File

@@ -1,127 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
import com.google.inject.ProvisionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.security.AccessToken;
import sonia.scm.security.CipherUtil;
import sonia.scm.security.Xsrf;
import sonia.scm.web.HgUtil;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*/
public final class HgEnvironment
{
private static final Logger LOG = LoggerFactory.getLogger(HgEnvironment.class);
/** Field description */
public static final String ENV_PYTHON_PATH = "PYTHONPATH";
/** Field description */
private static final String ENV_CHALLENGE = "SCM_CHALLENGE";
/** Field description */
private static final String ENV_URL = "SCM_URL";
private static final String SCM_BEARER_TOKEN = "SCM_BEARER_TOKEN";
private static final String SCM_XSRF = "SCM_XSRF";
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*/
private HgEnvironment() {}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param environment
* @param handler
* @param hookManager
*/
public static void prepareEnvironment(Map<String, String> environment,
HgRepositoryHandler handler, HgHookManager hookManager)
{
prepareEnvironment(environment, handler, hookManager, null);
}
/**
* Method description
*
*
* @param environment
* @param handler
* @param hookManager
* @param request
*/
public static void prepareEnvironment(Map<String, String> environment,
HgRepositoryHandler handler, HgHookManager hookManager,
HttpServletRequest request)
{
String hookUrl;
if (request != null)
{
hookUrl = hookManager.createUrl(request);
}
else
{
hookUrl = hookManager.createUrl();
}
try {
AccessToken accessToken = hookManager.getAccessToken();
environment.put(SCM_BEARER_TOKEN, CipherUtil.getInstance().encode(accessToken.compact()));
extractXsrfKey(environment, accessToken);
} catch (ProvisionException e) {
LOG.debug("could not create bearer token; looks like currently we are not in a request; probably you can ignore the following exception:", e);
}
environment.put(ENV_PYTHON_PATH, HgUtil.getPythonPath(handler.getConfig()));
environment.put(ENV_URL, hookUrl);
environment.put(ENV_CHALLENGE, hookManager.getChallenge());
}
private static void extractXsrfKey(Map<String, String> environment, AccessToken accessToken) {
environment.put(SCM_XSRF, accessToken.<String>getCustom(Xsrf.TOKEN_KEY).orElse("-"));
}
}

View File

@@ -21,28 +21,15 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import com.google.inject.servlet.RequestScoped;
import com.google.inject.ImplementedBy;
/**
* Holds an instance of {@link HgContext} in the request scope.
*
* <p>The problem seems to be that guice had multiple options for injecting HgContext. {@link HgContextProvider}
* bound via Module and {@link HgContext} bound void {@link RequestScoped} annotation. It looks like that Guice 4
* injects randomly the one or the other, in SCMv1 (Guice 3) everything works as expected.</p>
*
* <p>To fix the problem we have created this class annotated with {@link RequestScoped}, which holds an instance
* of {@link HgContext}. This way only the {@link HgContextProvider} is used for injection.</p>
*/
@RequestScoped
public class HgContextRequestStore {
private final HgContext context = new HgContext();
public HgContext get() {
return context;
}
import java.util.Map;
@ImplementedBy(DefaultHgEnvironmentBuilder.class)
public interface HgEnvironmentBuilder {
Map<String, String> read(Repository repository);
Map<String, String> write(Repository repository);
}

View File

@@ -1,354 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
import com.github.legman.Subscribe;
import com.google.common.base.MoreObjects;
import com.google.inject.Inject;
import com.google.inject.OutOfScopeException;
import com.google.inject.Provider;
import com.google.inject.ProvisionException;
import com.google.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.config.ScmConfigurationChangedEvent;
import sonia.scm.net.ahc.AdvancedHttpClient;
import sonia.scm.security.AccessToken;
import sonia.scm.security.AccessTokenBuilderFactory;
import sonia.scm.util.HttpUtil;
import sonia.scm.util.Util;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*/
@Singleton
public class HgHookManager {
@SuppressWarnings("java:S1075") // this url is fixed
private static final String URL_HOOKPATH = "/hook/hg/";
/**
* the logger for HgHookManager
*/
private static final Logger logger =
LoggerFactory.getLogger(HgHookManager.class);
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
* @param configuration
* @param httpServletRequestProvider
* @param httpClient
* @param accessTokenBuilderFactory
*/
@Inject
public HgHookManager(ScmConfiguration configuration,
Provider<HttpServletRequest> httpServletRequestProvider,
AdvancedHttpClient httpClient, AccessTokenBuilderFactory accessTokenBuilderFactory)
{
this.configuration = configuration;
this.httpServletRequestProvider = httpServletRequestProvider;
this.httpClient = httpClient;
this.accessTokenBuilderFactory = accessTokenBuilderFactory;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param config
*/
@Subscribe(async = false)
public void configChanged(ScmConfigurationChangedEvent config)
{
hookUrl = null;
}
/**
* Method description
*
*
* @param request
*
* @return
*/
public String createUrl(HttpServletRequest request)
{
if (hookUrl == null)
{
synchronized (this)
{
if (hookUrl == null)
{
buildHookUrl(request);
if (logger.isInfoEnabled() && Util.isNotEmpty(hookUrl))
{
logger.info("use {} for mercurial hooks", hookUrl);
}
}
}
}
return hookUrl;
}
/**
* Method description
*
*
* @return
*/
public String createUrl()
{
String url = hookUrl;
if (url == null)
{
HttpServletRequest request = getHttpServletRequest();
if (request != null)
{
url = createUrl(request);
}
else
{
url = createConfiguredUrl();
logger.warn(
"created url {} without request, in some cases this could cause problems",
url);
}
}
return url;
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
public String getChallenge()
{
return challenge;
}
/**
* Method description
*
*
* @param challenge
*
* @return
*/
public boolean isAcceptAble(String challenge)
{
return this.challenge.equals(challenge);
}
public AccessToken getAccessToken()
{
return accessTokenBuilderFactory.create().build();
}
private void buildHookUrl(HttpServletRequest request) {
if (configuration.isForceBaseUrl()) {
logger.debug("create hook url from configured base url because force base url is enabled");
hookUrl = createConfiguredUrl();
if (!isUrlWorking(hookUrl)) {
disableHooks();
}
} else {
logger.debug("create hook url from request");
hookUrl = HttpUtil.getCompleteUrl(request, URL_HOOKPATH);
if (!isUrlWorking(hookUrl)) {
logger.warn("hook url {} from request does not work, try now localhost", hookUrl);
hookUrl = createLocalUrl(request);
if (!isUrlWorking(hookUrl)) {
logger.warn("localhost hook url {} does not work, try now from configured base url", hookUrl);
hookUrl = createConfiguredUrl();
if (!isUrlWorking(hookUrl)) {
disableHooks();
}
}
}
}
}
/**
* Method description
*
*
* @return
*/
private String createConfiguredUrl()
{
//J-
return HttpUtil.getUriWithoutEndSeperator(
MoreObjects.firstNonNull(
configuration.getBaseUrl(),
"http://localhost:8080/scm"
)
).concat(URL_HOOKPATH);
//J+
}
/**
* Method description
*
*
* @param request
*
* @return
*/
private String createLocalUrl(HttpServletRequest request)
{
StringBuilder sb = new StringBuilder(request.getScheme());
sb.append("://localhost:").append(request.getLocalPort());
sb.append(request.getContextPath()).append(URL_HOOKPATH);
return sb.toString();
}
/**
* Method description
*
*/
private void disableHooks()
{
if (logger.isErrorEnabled())
{
logger.error(
"disabling mercurial hooks, because hook url {} seems not to work",
hookUrl);
}
hookUrl = Util.EMPTY_STRING;
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
private HttpServletRequest getHttpServletRequest()
{
HttpServletRequest request = null;
try
{
request = httpServletRequestProvider.get();
}
catch (ProvisionException | OutOfScopeException ex)
{
logger.debug("http servlet request is not available");
}
return request;
}
/**
* Method description
*
*
* @param url
*
* @return
*/
private boolean isUrlWorking(String url)
{
boolean result = false;
try
{
url = url.concat("?ping=true");
logger.trace("check hook url {}", url);
//J-
int sc = httpClient.get(url)
.disableHostnameValidation(true)
.disableCertificateValidation(true)
.ignoreProxySettings(true)
.disableTracing()
.request()
.getStatus();
//J+
result = sc == 204;
}
catch (IOException ex)
{
if (logger.isTraceEnabled())
{
logger.trace("url test failed for url ".concat(url), ex);
}
}
return result;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private String challenge = UUID.randomUUID().toString();
/** Field description */
private ScmConfiguration configuration;
/** Field description */
private volatile String hookUrl;
/** Field description */
private AdvancedHttpClient httpClient;
/** Field description */
private Provider<HttpServletRequest> httpServletRequestProvider;
private final AccessTokenBuilderFactory accessTokenBuilderFactory;
}

View File

@@ -38,80 +38,30 @@ import java.io.File;
*/
public enum HgPythonScript {
HOOK("scmhooks.py"), HGWEB("hgweb.py"), VERSION("version.py");
HOOK("scmhooks.py"), HGWEB("hgweb.py");
/** Field description */
private static final String BASE_DIRECTORY =
"lib".concat(File.separator).concat("python");
/** Field description */
private static final String BASE_DIRECTORY = "lib".concat(File.separator).concat("python");
private static final String BASE_RESOURCE = "/sonia/scm/python/";
//~--- constructors ---------------------------------------------------------
private final String name;
/**
* Constructs ...
*
*
* @param name
*/
private HgPythonScript(String name)
{
HgPythonScript(String name) {
this.name = name;
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @param context
*
* @return
*/
public static File getScriptDirectory(SCMContextProvider context)
{
public static File getScriptDirectory(SCMContextProvider context) {
return new File(context.getBaseDirectory(), BASE_DIRECTORY);
}
/**
* Method description
*
*
* @param context
*
* @return
*/
public File getFile(SCMContextProvider context)
{
public File getFile(SCMContextProvider context) {
return new File(getScriptDirectory(context), name);
}
/**
* Method description
*
*
* @return
*/
public String getName()
{
public String getName() {
return name;
}
/**
* Method description
*
*
* @return
*/
public String getResourcePath()
{
public String getResourcePath() {
return BASE_RESOURCE.concat(name);
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private String name;
}

View File

@@ -0,0 +1,106 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import com.aragost.javahg.RepositoryConfiguration;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.hooks.HookEnvironment;
import sonia.scm.repository.spi.javahg.HgFileviewExtension;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.File;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnsupportedCharsetException;
import java.util.Map;
import java.util.function.Function;
@Singleton
public class HgRepositoryFactory {
private static final Logger LOG = LoggerFactory.getLogger(HgRepositoryFactory.class);
private final HgRepositoryHandler handler;
private final HookEnvironment hookEnvironment;
private final HgEnvironmentBuilder environmentBuilder;
private final Function<Repository, File> directoryResolver;
@Inject
public HgRepositoryFactory(HgRepositoryHandler handler, HookEnvironment hookEnvironment, HgEnvironmentBuilder environmentBuilder) {
this(
handler, hookEnvironment, environmentBuilder,
repository -> handler.getDirectory(repository.getId())
);
}
@VisibleForTesting
public HgRepositoryFactory(HgRepositoryHandler handler, HookEnvironment hookEnvironment, HgEnvironmentBuilder environmentBuilder, Function<Repository, File> directoryResolver) {
this.handler = handler;
this.hookEnvironment = hookEnvironment;
this.environmentBuilder = environmentBuilder;
this.directoryResolver = directoryResolver;
}
public com.aragost.javahg.Repository openForRead(Repository repository) {
return open(repository, environmentBuilder.read(repository));
}
public com.aragost.javahg.Repository openForWrite(Repository repository) {
return open(repository, environmentBuilder.write(repository));
}
private com.aragost.javahg.Repository open(Repository repository, Map<String, String> environment) {
File directory = directoryResolver.apply(repository);
RepositoryConfiguration repoConfiguration = RepositoryConfiguration.DEFAULT;
repoConfiguration.getEnvironment().putAll(environment);
repoConfiguration.addExtension(HgFileviewExtension.class);
boolean pending = hookEnvironment.isPending();
repoConfiguration.setEnablePendingChangesets(pending);
Charset encoding = encoding();
repoConfiguration.setEncoding(encoding);
repoConfiguration.setHgBin(handler.getConfig().getHgBinary());
LOG.trace("open hg repository {}: encoding: {}, pending: {}", directory, encoding, pending);
return com.aragost.javahg.Repository.open(repoConfiguration, directory);
}
private Charset encoding() {
String charset = handler.getConfig().getEncoding();
try {
return Charset.forName(charset);
} catch (UnsupportedCharsetException ex) {
LOG.warn("unknown charset {} in hg config, fallback to utf-8", charset);
return StandardCharsets.UTF_8;
}
}
}

View File

@@ -27,11 +27,9 @@ package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.ConfigurationException;
import sonia.scm.SCMContextProvider;
import sonia.scm.autoconfig.AutoConfigurator;
import sonia.scm.installer.HgInstaller;
@@ -43,14 +41,14 @@ import sonia.scm.io.INISection;
import sonia.scm.plugin.Extension;
import sonia.scm.plugin.PluginLoader;
import sonia.scm.repository.spi.HgRepositoryServiceProvider;
import sonia.scm.repository.spi.HgVersionCommand;
import sonia.scm.repository.spi.HgWorkingCopyFactory;
import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.util.IOUtil;
import sonia.scm.util.SystemUtil;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -63,14 +61,15 @@ import java.util.Optional;
public class HgRepositoryHandler
extends AbstractSimpleRepositoryHandler<HgConfig> {
public static final String PATH_HOOK = ".hook-1.8";
public static final String RESOURCE_VERSION = "sonia/scm/version/scm-hg-plugin";
public static final String TYPE_DISPLAYNAME = "Mercurial";
public static final String TYPE_NAME = "hg";
public static final RepositoryType TYPE = new RepositoryType(TYPE_NAME,
public static final RepositoryType TYPE = new RepositoryType(
TYPE_NAME,
TYPE_DISPLAYNAME,
HgRepositoryServiceProvider.COMMANDS,
HgRepositoryServiceProvider.FEATURES);
HgRepositoryServiceProvider.FEATURES
);
private static final Logger logger = LoggerFactory.getLogger(HgRepositoryHandler.class);
@@ -78,28 +77,14 @@ public class HgRepositoryHandler
private static final String CONFIG_SECTION_SCMM = "scmm";
private static final String CONFIG_KEY_REPOSITORY_ID = "repositoryid";
private final Provider<HgContext> hgContextProvider;
private final HgWorkingCopyFactory workingCopyFactory;
private final JAXBContext jaxbContext;
@Inject
public HgRepositoryHandler(ConfigurationStoreFactory storeFactory,
Provider<HgContext> hgContextProvider,
RepositoryLocationResolver repositoryLocationResolver,
PluginLoader pluginLoader, HgWorkingCopyFactory workingCopyFactory) {
super(storeFactory, repositoryLocationResolver, pluginLoader);
this.hgContextProvider = hgContextProvider;
this.workingCopyFactory = workingCopyFactory;
try {
this.jaxbContext = JAXBContext.newInstance(BrowserResult.class,
BlameResult.class, Changeset.class, ChangesetPagingResult.class,
HgVersion.class);
} catch (JAXBException ex) {
throw new ConfigurationException("could not create jaxbcontext", ex);
}
}
public void doAutoConfiguration(HgConfig autoConfig) {
@@ -107,8 +92,7 @@ public class HgRepositoryHandler
try {
if (logger.isDebugEnabled()) {
logger.debug("installing mercurial with {}",
installer.getClass().getName());
logger.debug("installing mercurial with {}", installer.getClass().getName());
}
installer.install(baseDirectory, autoConfig);
@@ -154,16 +138,6 @@ public class HgRepositoryHandler
}
}
public HgContext getHgContext() {
HgContext context = hgContextProvider.get();
if (context == null) {
context = new HgContext();
}
return context;
}
@Override
public ImportHandler getImportHandler() {
return new HgImportHandler(this);
@@ -176,28 +150,14 @@ public class HgRepositoryHandler
@Override
public String getVersionInformation() {
String version = getStringFromResource(RESOURCE_VERSION,
DEFAULT_VERSION_INFORMATION);
return getVersionInformation(new HgVersionCommand(getConfig()));
}
try {
HgVersion hgVersion = new HgVersionHandler(this, hgContextProvider.get(),
baseDirectory).getVersion();
if (hgVersion != null) {
if (logger.isDebugEnabled()) {
logger.debug("mercurial/python informations: {}", hgVersion);
}
version = MessageFormat.format(version, hgVersion.getPython(),
hgVersion.getMercurial());
} else if (logger.isWarnEnabled()) {
logger.warn("could not retrieve version informations");
}
} catch (Exception ex) {
logger.error("could not read version informations", ex);
}
return version;
String getVersionInformation(HgVersionCommand command) {
String version = getStringFromResource(RESOURCE_VERSION, DEFAULT_VERSION_INFORMATION);
HgVersion hgVersion = command.get();
logger.debug("mercurial/python informations: {}", hgVersion);
return MessageFormat.format(version, hgVersion.getPython(), hgVersion.getMercurial());
}
@Override
@@ -253,28 +213,24 @@ public class HgRepositoryHandler
logger.debug("write python script {}", script.getName());
}
InputStream content = null;
OutputStream output = null;
try {
content = HgRepositoryHandler.class.getResourceAsStream(
script.getResourcePath());
output = new FileOutputStream(script.getFile(context));
try (InputStream content = input(script); OutputStream output = output(context, script)) {
IOUtil.copy(content, output);
} catch (IOException ex) {
logger.error("could not write script", ex);
} finally {
IOUtil.close(content);
IOUtil.close(output);
}
}
}
private InputStream input(HgPythonScript script) {
return HgRepositoryHandler.class.getResourceAsStream(script.getResourcePath());
}
private OutputStream output(SCMContextProvider context, HgPythonScript script) throws FileNotFoundException {
return new FileOutputStream(script.getFile(context));
}
public HgWorkingCopyFactory getWorkingCopyFactory() {
return workingCopyFactory;
}
public JAXBContext getJaxbContext() {
return jaxbContext;
}
}

View File

@@ -24,10 +24,8 @@
package sonia.scm.repository;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.AllArgsConstructor;
import lombok.Data;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
@@ -37,13 +35,14 @@ import javax.xml.bind.annotation.XmlRootElement;
*
* @author Sebastian Sdorra
*/
@Data
@AllArgsConstructor
@XmlRootElement(name = "version")
@XmlAccessorType(XmlAccessType.FIELD)
@EqualsAndHashCode
@Getter
@Setter
@ToString
public class HgVersion {
public static final String UNKNOWN = "x.y.z (unknown)";
private String mercurial;
private String python;
}

View File

@@ -21,75 +21,31 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.api;
//~--- JDK imports ------------------------------------------------------------
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
*
* @author Sebastian Sdorra
*/
public final class HgHookMessage implements Serializable
{
@Getter
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public final class HgHookMessage implements Serializable {
/** Field description */
private static final long serialVersionUID = 1804492842452344326L;
//~--- constant enums -------------------------------------------------------
/**
* Enum description
*
*/
public static enum Severity { NOTE, ERROR; }
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*
* @param severity
* @param message
*/
public HgHookMessage(Severity severity, String message)
{
this.severity = severity;
this.message = message;
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
public String getMessage()
{
return message;
}
/**
* Method description
*
*
* @return
*/
public Severity getSeverity()
{
return severity;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private Severity severity;
private String message;
/** Field description */
private Severity severity;
public enum Severity { NOTE, ERROR }
}

View File

@@ -0,0 +1,188 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.hooks;
import com.google.inject.assistedinject.Assisted;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.ExceptionWithContext;
import sonia.scm.NotFoundException;
import sonia.scm.TransactionId;
import sonia.scm.repository.RepositoryHookType;
import sonia.scm.repository.api.HgHookMessage;
import sonia.scm.repository.spi.HgHookContextProvider;
import sonia.scm.repository.spi.HookEventFacade;
import sonia.scm.security.BearerToken;
import sonia.scm.security.CipherUtil;
import javax.annotation.Nonnull;
import javax.inject.Inject;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import static java.util.Collections.singletonList;
class DefaultHookHandler implements HookHandler {
private static final Logger LOG = LoggerFactory.getLogger(DefaultHookHandler.class);
private final HookEventFacade hookEventFacade;
private final HookEnvironment environment;
private final HookContextProviderFactory hookContextProviderFactory;
private final Socket socket;
@Inject
public DefaultHookHandler(HookContextProviderFactory hookContextProviderFactory, HookEventFacade hookEventFacade, HookEnvironment environment, @Assisted Socket socket) {
this.hookContextProviderFactory = hookContextProviderFactory;
this.hookEventFacade = hookEventFacade;
this.environment = environment;
this.socket = socket;
}
@Override
public void run() {
LOG.trace("start handling hook protocol");
try (InputStream input = socket.getInputStream(); OutputStream output = socket.getOutputStream()) {
handleHookRequest(input, output);
} catch (IOException e) {
LOG.warn("failed to read hook request", e);
} finally {
LOG.trace("close client socket");
TransactionId.clear();
close();
}
}
private void handleHookRequest(InputStream input, OutputStream output) throws IOException {
Request request = Sockets.receive(input, Request.class);
TransactionId.set(request.getTransactionId());
Response response = handleHookRequest(request);
Sockets.send(output, response);
}
private Response handleHookRequest(Request request) {
LOG.trace("process {} hook for node {}", request.getType(), request.getNode());
if (!environment.isAcceptAble(request.getChallenge())) {
LOG.warn("received hook with invalid challenge: {}", request.getChallenge());
return error("invalid hook challenge");
}
try {
authenticate(request);
return fireHook(request);
} catch (AuthenticationException ex) {
LOG.warn("hook authentication failed", ex);
return error("hook authentication failed");
}
}
@Nonnull
private Response fireHook(Request request) {
HgHookContextProvider context = hookContextProviderFactory.create(request.getRepositoryId(), request.getNode());
try {
environment.setPending(request.getType() == RepositoryHookType.PRE_RECEIVE);
hookEventFacade.handle(request.getRepositoryId()).fireHookEvent(request.getType(), context);
return new Response(context.getHgMessageProvider().getMessages(), false);
} catch (NotFoundException ex) {
LOG.warn("could not find repository with id {}", request.getRepositoryId(), ex);
return error("repository not found");
} catch (ExceptionWithContext ex) {
LOG.debug("scm exception on hook occurred", ex);
return error(context, ex.getMessage());
} catch (Exception ex) {
LOG.warn("unknown error on hook occurred", ex);
return error(context, "unknown error");
} finally {
environment.clearPendingState();
}
}
private void authenticate(Request request) {
LOG.trace("authenticate hook request");
String token = CipherUtil.getInstance().decode(request.getToken());
BearerToken bearer = BearerToken.valueOf(token);
Subject subject = SecurityUtils.getSubject();
subject.login(bearer);
}
private Response error(HgHookContextProvider context, String message) {
List<HgHookMessage> messages = new ArrayList<>(context.getHgMessageProvider().getMessages());
messages.add(createErrorMessage(message));
return new Response(messages, true);
}
private Response error(String message) {
return new Response(
singletonList(createErrorMessage(message)),
true
);
}
@Nonnull
private HgHookMessage createErrorMessage(String message) {
return new HgHookMessage(HgHookMessage.Severity.ERROR, message);
}
private void close() {
try {
socket.close();
} catch (IOException e) {
LOG.debug("failed to close hook socket", e);
}
}
@Data
@AllArgsConstructor
public static class Request {
private String token;
private RepositoryHookType type;
private String transactionId;
private String repositoryId;
private String challenge;
private String node;
}
@Data
@AllArgsConstructor
public static class Response {
private List<HgHookMessage> messages;
private boolean abort;
}
}

View File

@@ -21,60 +21,37 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
package sonia.scm.repository.hooks;
import sonia.scm.repository.AbstractHgHandler;
import sonia.scm.repository.HgContext;
import sonia.scm.NotFoundException;
import sonia.scm.repository.HgRepositoryFactory;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.spi.HgHookContextProvider;
//~--- JDK imports ------------------------------------------------------------
import javax.inject.Inject;
import java.io.File;
public class HookContextProviderFactory {
import java.util.Map;
private final RepositoryManager repositoryManager;
private final HgRepositoryHandler repositoryHandler;
private final HgRepositoryFactory repositoryFactory;
/**
*
* @author Sebastian Sdorra
*/
public class AbstractHgCommand extends AbstractHgHandler
{
/**
* Constructs ...
*
*
* @param handler
* @param context
* @param repository
* @param repositoryDirectory
*/
protected AbstractHgCommand(HgRepositoryHandler handler, HgContext context,
Repository repository, File repositoryDirectory)
{
super(handler, context, repository, repositoryDirectory);
@Inject
public HookContextProviderFactory(RepositoryManager repositoryManager, HgRepositoryHandler repositoryHandler, HgRepositoryFactory repositoryFactory) {
this.repositoryManager = repositoryManager;
this.repositoryHandler = repositoryHandler;
this.repositoryFactory = repositoryFactory;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param revision
* @param path
*
* @param request
*
* @return
*/
protected Map<String,
String> createEnvironment(FileBaseCommandRequest request)
{
return createEnvironment(request.getRevision(), request.getPath());
HgHookContextProvider create(String repositoryId, String node) {
Repository repository = repositoryManager.get(repositoryId);
if (repository == null) {
throw new NotFoundException(Repository.class, repositoryId);
}
return new HgHookContextProvider(repositoryHandler, repositoryFactory, repository, node);
}
}

View File

@@ -0,0 +1,60 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.hooks;
import javax.inject.Singleton;
import java.util.UUID;
@Singleton
public class HookEnvironment {
private final ThreadLocal<Boolean> threadEnvironment = new ThreadLocal<>();
private final String challenge = UUID.randomUUID().toString();
public String getChallenge() {
return challenge;
}
public boolean isAcceptAble(String challenge) {
return this.challenge.equals(challenge);
}
void setPending(boolean pending) {
threadEnvironment.set(pending);
}
void clearPendingState() {
threadEnvironment.remove();
}
public boolean isPending() {
Boolean threadState = threadEnvironment.get();
if (threadState != null) {
return threadState;
}
return false;
}
}

View File

@@ -0,0 +1,28 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.hooks;
public interface HookHandler extends Runnable {
}

View File

@@ -0,0 +1,34 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.hooks;
import java.net.Socket;
@FunctionalInterface
interface HookHandlerFactory {
HookHandler create(Socket socket);
}

View File

@@ -0,0 +1,40 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.hooks;
import com.google.inject.AbstractModule;
import com.google.inject.assistedinject.FactoryModuleBuilder;
import sonia.scm.plugin.Extension;
@Extension
public class HookModule extends AbstractModule {
@Override
protected void configure() {
install(new FactoryModuleBuilder()
.implement(HookHandler.class, DefaultHookHandler.class)
.build(HookHandlerFactory.class)
);
}
}

View File

@@ -0,0 +1,153 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.hooks;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.util.ThreadContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
@Singleton
public class HookServer implements AutoCloseable {
private static final Logger LOG = LoggerFactory.getLogger(HookServer.class);
private final HookHandlerFactory handlerFactory;
private ExecutorService acceptor;
private ExecutorService workerPool;
private ServerSocket serverSocket;
private SecurityManager securityManager;
@Inject
public HookServer(HookHandlerFactory handlerFactory) {
this.handlerFactory = handlerFactory;
}
public int start() throws IOException {
securityManager = SecurityUtils.getSecurityManager();
acceptor = createAcceptor();
workerPool = createWorkerPool();
serverSocket = createServerSocket();
// set timeout to 2 min, to avoid blocking clients
serverSocket.setSoTimeout(2 * 60 * 1000);
accept();
int port = serverSocket.getLocalPort();
LOG.info("open hg hook server on port {}", port);
return port;
}
private void accept() {
acceptor.submit(() -> {
while (!serverSocket.isClosed()) {
try {
LOG.trace("wait for next hook connection");
Socket clientSocket = serverSocket.accept();
LOG.trace("accept incoming hook client from {}", clientSocket.getInetAddress());
HookHandler hookHandler = handlerFactory.create(clientSocket);
workerPool.submit(associateSecurityManager(hookHandler));
} catch (IOException ex) {
LOG.debug("failed to accept socket, possible closed", ex);
}
}
LOG.warn("ServerSocket is closed");
});
}
private Runnable associateSecurityManager(HookHandler hookHandler) {
return () -> {
ThreadContext.bind(securityManager);
try {
hookHandler.run();
} finally {
ThreadContext.unbindSubject();
ThreadContext.unbindSecurityManager();
}
};
}
@Nonnull
private ServerSocket createServerSocket() throws IOException {
return new ServerSocket(0, 0, InetAddress.getLoopbackAddress());
}
private ExecutorService createAcceptor() {
return Executors.newSingleThreadExecutor(
createThreadFactory("HgHookAcceptor")
);
}
private ExecutorService createWorkerPool() {
return Executors.newCachedThreadPool(
createThreadFactory("HgHookWorker-%d")
);
}
@Nonnull
private ThreadFactory createThreadFactory(String hgHookAcceptor) {
return new ThreadFactoryBuilder()
.setNameFormat(hgHookAcceptor)
.build();
}
@Override
public void close() {
closeSocket();
shutdown(acceptor);
shutdown(workerPool);
}
private void closeSocket() {
if (serverSocket != null) {
try {
serverSocket.close();
} catch (IOException ex) {
LOG.warn("failed to close server socket", ex);
}
}
}
private void shutdown(ExecutorService acceptor) {
if (acceptor != null) {
acceptor.shutdown();
}
}
}

View File

@@ -0,0 +1,81 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.hooks;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
class Sockets {
private static final Logger LOG = LoggerFactory.getLogger(Sockets.class);
private static final int READ_LIMIT = 8192;
private static final ObjectMapper objectMapper = new ObjectMapper();
private Sockets() {
}
static void send(OutputStream out, Object object) throws IOException {
byte[] bytes = objectMapper.writeValueAsBytes(object);
LOG.trace("send message length of {} to socket", bytes.length);
DataOutputStream dataOutputStream = new DataOutputStream(out);
dataOutputStream.writeInt(bytes.length);
LOG.trace("send message to socket");
dataOutputStream.write(bytes);
LOG.trace("flush socket");
out.flush();
}
static <T> T receive(InputStream in, Class<T> type) throws IOException {
LOG.trace("read {} from socket", type);
DataInputStream dataInputStream = new DataInputStream(in);
int length = dataInputStream.readInt();
LOG.trace("read message length of {} from socket", length);
if (length > READ_LIMIT) {
String message = String.format("received length of %d, which exceeds the limit of %d", length, READ_LIMIT);
throw new IOException(message);
}
byte[] data = new byte[length];
dataInputStream.readFully(data);
LOG.trace("convert message to {}", type);
return objectMapper.readValue(data, type);
}
}

View File

@@ -27,18 +27,12 @@ package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
import com.aragost.javahg.Repository;
import com.google.common.base.Strings;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.HgRepositoryFactory;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.RepositoryProvider;
import sonia.scm.web.HgUtil;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.function.BiConsumer;
//~--- JDK imports ------------------------------------------------------------
@@ -46,105 +40,32 @@ import java.util.function.BiConsumer;
*
* @author Sebastian Sdorra
*/
public class HgCommandContext implements Closeable, RepositoryProvider
{
public class HgCommandContext implements Closeable, RepositoryProvider {
/** Field description */
private static final String PROPERTY_ENCODING = "hg.encoding";
private final HgRepositoryHandler handler;
private final HgRepositoryFactory factory;
private final sonia.scm.repository.Repository scmRepository;
//~--- constructors ---------------------------------------------------------
private Repository repository;
/**
* Constructs ...
*
*
* @param hookManager
* @param handler
* @param repository
* @param directory
*/
public HgCommandContext(HgHookManager hookManager,
HgRepositoryHandler handler, sonia.scm.repository.Repository repository,
File directory)
{
this(hookManager, handler, repository, directory,
handler.getHgContext().isPending());
}
/**
* Constructs ...
*
*
* @param hookManager
* @param handler
* @param repository
* @param directory
* @param pending
*/
public HgCommandContext(HgHookManager hookManager,
HgRepositoryHandler handler, sonia.scm.repository.Repository repository,
File directory, boolean pending)
{
this.hookManager = hookManager;
public HgCommandContext(HgRepositoryHandler handler, HgRepositoryFactory factory, sonia.scm.repository.Repository scmRepository) {
this.handler = handler;
this.directory = directory;
this.scmRepository = repository;
this.encoding = repository.getProperty(PROPERTY_ENCODING);
this.pending = pending;
if (Strings.isNullOrEmpty(encoding))
{
encoding = handler.getConfig().getEncoding();
}
this.factory = factory;
this.scmRepository = scmRepository;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @throws IOException
*/
@Override
public void close() throws IOException
{
if (repository != null)
{
repository.close();
public Repository open() {
if (repository == null) {
repository = factory.openForRead(scmRepository);
}
}
/**
* Method description
*
*
* @return
*/
public Repository open()
{
if (repository == null)
{
repository = HgUtil.open(handler, hookManager, directory, encoding, pending);
}
return repository;
}
public Repository openWithSpecialEnvironment(BiConsumer<sonia.scm.repository.Repository, Map<String, String>> prepareEnvironment)
{
return HgUtil.open(handler, directory, encoding,
pending, environment -> prepareEnvironment.accept(scmRepository, environment));
public Repository openForWrite() {
return factory.openForWrite(scmRepository);
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
public HgConfig getConfig()
{
return handler.getConfig();
@@ -159,25 +80,12 @@ public class HgCommandContext implements Closeable, RepositoryProvider
return getScmRepository();
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private File directory;
@Override
public void close() {
if (repository != null) {
repository.close();
}
}
/** Field description */
private String encoding;
/** Field description */
private HgRepositoryHandler handler;
/** Field description */
private HgHookManager hookManager;
/** Field description */
private boolean pending;
/** Field description */
private Repository repository;
private final sonia.scm.repository.Repository scmRepository;
}

View File

@@ -21,85 +21,56 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
package sonia.scm.repository.spi;
import com.aragost.javahg.Repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.HgRepositoryFactory;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.RepositoryHookType;
import sonia.scm.repository.spi.javahg.HgLogChangesetCommand;
import sonia.scm.web.HgUtil;
import java.io.File;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*/
public class HgHookChangesetProvider implements HookChangesetProvider
{
public class HgHookChangesetProvider implements HookChangesetProvider {
/**
* the logger for HgHookChangesetProvider
*/
private static final Logger logger =
LoggerFactory.getLogger(HgHookChangesetProvider.class);
private static final Logger LOG = LoggerFactory.getLogger(HgHookChangesetProvider.class);
//~--- constructors ---------------------------------------------------------
private final HgRepositoryHandler handler;
private final HgRepositoryFactory factory;
private final sonia.scm.repository.Repository scmRepository;
private final String startRev;
public HgHookChangesetProvider(HgRepositoryHandler handler,
File repositoryDirectory, HgHookManager hookManager, String startRev,
RepositoryHookType type)
{
private HookChangesetResponse response;
public HgHookChangesetProvider(HgRepositoryHandler handler, HgRepositoryFactory factory, sonia.scm.repository.Repository scmRepository, String startRev) {
this.handler = handler;
this.repositoryDirectory = repositoryDirectory;
this.hookManager = hookManager;
this.factory = factory;
this.scmRepository = scmRepository;
this.startRev = startRev;
this.type = type;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param request
*
* @return
*/
@Override
public synchronized HookChangesetResponse handleRequest(HookChangesetRequest request)
{
if (response == null)
{
public synchronized HookChangesetResponse handleRequest(HookChangesetRequest request) {
if (response == null) {
Repository repository = null;
try
{
repository = open();
try {
repository = factory.openForRead(scmRepository);
HgLogChangesetCommand cmd = HgLogChangesetCommand.on(repository,
handler.getConfig());
HgLogChangesetCommand cmd = HgLogChangesetCommand.on(repository, handler.getConfig());
response = new HookChangesetResponse(
cmd.rev(startRev.concat(":").concat(HgUtil.REVISION_TIP)).execute());
}
catch (Exception ex)
{
logger.error("could not retrieve changesets", ex);
}
finally
{
if (repository != null)
{
cmd.rev(startRev.concat(":").concat(HgUtil.REVISION_TIP)).execute()
);
} catch (Exception ex) {
LOG.error("could not retrieve changesets", ex);
} finally {
if (repository != null) {
repository.close();
}
}
@@ -108,39 +79,4 @@ public class HgHookChangesetProvider implements HookChangesetProvider
return response;
}
/**
* Method description
*
*
* @return
*/
private Repository open()
{
// use HG_PENDING only for pre receive hooks
boolean pending = type == RepositoryHookType.PRE_RECEIVE;
// TODO get repository encoding
return HgUtil.open(handler, hookManager, repositoryDirectory, null,
pending);
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private HgRepositoryHandler handler;
/** Field description */
private HgHookManager hookManager;
/** Field description */
private File repositoryDirectory;
/** Field description */
private HookChangesetResponse response;
/** Field description */
private String startRev;
/** Field description */
private RepositoryHookType type;
}

View File

@@ -21,14 +21,14 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.HgRepositoryFactory;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.RepositoryHookType;
import sonia.scm.repository.Repository;
import sonia.scm.repository.api.HgHookBranchProvider;
import sonia.scm.repository.api.HgHookMessageProvider;
import sonia.scm.repository.api.HgHookTagProvider;
@@ -37,7 +37,6 @@ import sonia.scm.repository.api.HookFeature;
import sonia.scm.repository.api.HookMessageProvider;
import sonia.scm.repository.api.HookTagProvider;
import java.io.File;
import java.util.EnumSet;
import java.util.Set;
@@ -45,55 +44,40 @@ import java.util.Set;
/**
* Mercurial implementation of {@link HookContextProvider}.
*
*
* @author Sebastian Sdorra
*/
public class HgHookContextProvider extends HookContextProvider
{
public class HgHookContextProvider extends HookContextProvider {
private static final Set<HookFeature> SUPPORTED_FEATURES =
EnumSet.of(HookFeature.CHANGESET_PROVIDER, HookFeature.MESSAGE_PROVIDER,
HookFeature.BRANCH_PROVIDER, HookFeature.TAG_PROVIDER);
private static final Set<HookFeature> SUPPORTED_FEATURES = EnumSet.of(
HookFeature.CHANGESET_PROVIDER,
HookFeature.MESSAGE_PROVIDER,
HookFeature.BRANCH_PROVIDER,
HookFeature.TAG_PROVIDER
);
//~--- constructors ---------------------------------------------------------
private final HgHookChangesetProvider hookChangesetProvider;
private HgHookMessageProvider hgMessageProvider;
private HgHookBranchProvider hookBranchProvider;
private HgHookTagProvider hookTagProvider;
/**
* Constructs a new instance.
*
* @param handler mercurial repository handler
* @param repositoryDirectory the directory of the changed repository
* @param hookManager mercurial hook manager
* @param startRev start revision
* @param type type of hook
*/
public HgHookContextProvider(HgRepositoryHandler handler,
File repositoryDirectory, HgHookManager hookManager, String startRev,
RepositoryHookType type)
{
this.hookChangesetProvider = new HgHookChangesetProvider(handler, repositoryDirectory, hookManager, startRev, type);
public HgHookContextProvider(HgRepositoryHandler handler, HgRepositoryFactory factory, Repository repository, String startRev) {
this.hookChangesetProvider = new HgHookChangesetProvider(handler, factory, repository, startRev);
}
//~--- get methods ----------------------------------------------------------
@Override
public HookBranchProvider getBranchProvider()
{
if (hookBranchProvider == null)
{
public HookBranchProvider getBranchProvider() {
if (hookBranchProvider == null) {
hookBranchProvider = new HgHookBranchProvider(hookChangesetProvider);
}
return hookBranchProvider;
}
@Override
public HookTagProvider getTagProvider()
{
if (hookTagProvider == null)
{
public HookTagProvider getTagProvider() {
if (hookTagProvider == null) {
hookTagProvider = new HgHookTagProvider(hookChangesetProvider);
}
return hookTagProvider;
}
@@ -102,14 +86,11 @@ public class HgHookContextProvider extends HookContextProvider
{
return hookChangesetProvider;
}
public HgHookMessageProvider getHgMessageProvider()
{
if (hgMessageProvider == null)
{
public HgHookMessageProvider getHgMessageProvider() {
if (hgMessageProvider == null) {
hgMessageProvider = new HgHookMessageProvider();
}
return hgMessageProvider;
}
@@ -119,21 +100,9 @@ public class HgHookContextProvider extends HookContextProvider
return SUPPORTED_FEATURES;
}
//~--- methods --------------------------------------------------------------
@Override
protected HookMessageProvider createMessageProvider()
{
return getHgMessageProvider();
}
//~--- fields ---------------------------------------------------------------
private final HgHookChangesetProvider hookChangesetProvider;
private HgHookMessageProvider hgMessageProvider;
private HgHookBranchProvider hookBranchProvider;
private HgHookTagProvider hookTagProvider;
}

View File

@@ -31,6 +31,8 @@ import com.aragost.javahg.commands.ExecutionException;
import com.aragost.javahg.commands.PullCommand;
import com.aragost.javahg.commands.RemoveCommand;
import com.aragost.javahg.commands.StatusCommand;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.NoChangesMadeException;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.work.WorkingCopy;
@@ -41,11 +43,13 @@ import java.nio.file.Path;
import java.util.List;
import java.util.regex.Pattern;
@SuppressWarnings("java:S3252") // it is ok for javahg classes to access static method of subtype
public class HgModifyCommand implements ModifyCommand {
private static final Logger LOG = LoggerFactory.getLogger(HgModifyCommand.class);
static final Pattern HG_MESSAGE_PATTERN = Pattern.compile(".*\\[SCM\\](?: Error:)? (.*)");
private HgCommandContext context;
private final HgCommandContext context;
private final HgWorkingCopyFactory workingCopyFactory;
public HgModifyCommand(HgCommandContext context, HgWorkingCopyFactory workingCopyFactory) {
@@ -55,7 +59,6 @@ public class HgModifyCommand implements ModifyCommand {
@Override
public String execute(ModifyCommandRequest request) {
try (WorkingCopy<com.aragost.javahg.Repository, com.aragost.javahg.Repository> workingCopy = workingCopyFactory.createWorkingCopy(context, request.getBranch())) {
Repository workingRepository = workingCopy.getWorkingRepository();
request.getRequests().forEach(
@@ -100,12 +103,21 @@ public class HgModifyCommand implements ModifyCommand {
}
}
);
if (StatusCommand.on(workingRepository).lines().isEmpty()) {
throw new NoChangesMadeException(context.getScmRepository());
}
CommitCommand.on(workingRepository).user(String.format("%s <%s>", request.getAuthor().getName(), request.getAuthor().getMail())).message(request.getCommitMessage()).execute();
LOG.trace("commit changes in working copy");
CommitCommand.on(workingRepository)
.user(String.format("%s <%s>", request.getAuthor().getName(), request.getAuthor().getMail()))
.message(request.getCommitMessage()).execute();
List<Changeset> execute = pullModifyChangesToCentralRepository(request, workingCopy);
return execute.get(0).getNode();
String node = execute.get(0).getNode();
LOG.debug("successfully pulled changes from working copy, new node {}", node);
return node;
} catch (ExecutionException e) {
throwInternalRepositoryException("could not execute command on repository", e);
return null;
@@ -113,6 +125,7 @@ public class HgModifyCommand implements ModifyCommand {
}
private List<Changeset> pullModifyChangesToCentralRepository(ModifyCommandRequest request, WorkingCopy<com.aragost.javahg.Repository, com.aragost.javahg.Repository> workingCopy) {
LOG.trace("pull changes from working copy");
try {
com.aragost.javahg.commands.PullCommand pullCommand = PullCommand.on(workingCopy.getCentralRepository());
workingCopyFactory.configure(pullCommand);

View File

@@ -26,13 +26,12 @@ package sonia.scm.repository.spi;
import com.google.common.io.Closeables;
import sonia.scm.repository.Feature;
import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.HgRepositoryFactory;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.Repository;
import sonia.scm.repository.api.Command;
import sonia.scm.repository.api.CommandNotSupportedException;
import java.io.File;
import java.io.IOException;
import java.util.EnumSet;
import java.util.Set;
@@ -41,11 +40,8 @@ import java.util.Set;
*
* @author Sebastian Sdorra
*/
public class HgRepositoryServiceProvider extends RepositoryServiceProvider
{
public class HgRepositoryServiceProvider extends RepositoryServiceProvider {
/** Field description */
//J-
public static final Set<Command> COMMANDS = EnumSet.of(
Command.BLAME,
Command.BROWSE,
@@ -61,25 +57,19 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider
Command.PULL,
Command.MODIFY
);
//J+
/** Field description */
public static final Set<Feature> FEATURES =
EnumSet.of(Feature.COMBINED_DEFAULT_BRANCH);
public static final Set<Feature> FEATURES = EnumSet.of(Feature.COMBINED_DEFAULT_BRANCH);
//~--- constructors ---------------------------------------------------------
private final HgRepositoryHandler handler;
private final HgCommandContext context;
HgRepositoryServiceProvider(HgRepositoryHandler handler,
HgHookManager hookManager, Repository repository)
{
HgRepositoryServiceProvider(HgRepositoryHandler handler, HgRepositoryFactory factory, Repository repository) {
this.handler = handler;
this.repositoryDirectory = handler.getDirectory(repository.getId());
this.context = new HgCommandContext(hookManager, handler, repository,
repositoryDirectory);
this.context = new HgCommandContext(handler, factory, repository);
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
@@ -91,9 +81,9 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider
{
Closeables.close(context, true);
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
@@ -271,14 +261,4 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider
return new HgTagsCommand(context);
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private HgCommandContext context;
/** Field description */
private HgRepositoryHandler handler;
/** Field description */
private File repositoryDirectory;
}

View File

@@ -21,12 +21,12 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.spi;
import com.google.inject.Inject;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.HgRepositoryFactory;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.Repository;
@@ -35,18 +35,15 @@ import sonia.scm.repository.Repository;
* @author Sebastian Sdorra
*/
@Extension
public class HgRepositoryServiceResolver implements RepositoryServiceResolver
{
public class HgRepositoryServiceResolver implements RepositoryServiceResolver {
private final HgRepositoryHandler handler;
private final HgHookManager hookManager;
private final HgRepositoryFactory factory;
@Inject
public HgRepositoryServiceResolver(HgRepositoryHandler handler,
HgHookManager hookManager)
{
public HgRepositoryServiceResolver(HgRepositoryHandler handler, HgRepositoryFactory factory) {
this.handler = handler;
this.hookManager = hookManager;
this.factory = factory;
}
@Override
@@ -54,7 +51,7 @@ public class HgRepositoryServiceResolver implements RepositoryServiceResolver
HgRepositoryServiceProvider provider = null;
if (HgRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) {
provider = new HgRepositoryServiceProvider(handler, hookManager, repository);
provider = new HgRepositoryServiceProvider(handler, factory, repository);
}
return provider;

View File

@@ -0,0 +1,120 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.spi;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.io.ByteStreams;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgVersion;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class HgVersionCommand {
private static final Logger LOG = LoggerFactory.getLogger(HgVersionCommand.class);
@VisibleForTesting
static final String[] HG_ARGS = {
"version", "--template", "{ver}"
};
@VisibleForTesting
static final String[] PYTHON_ARGS = {
"-c", "import sys; print(sys.version)"
};
private final HgConfig config;
private final ProcessExecutor executor;
public HgVersionCommand(HgConfig config) {
this(config, command -> new ProcessBuilder(command).start());
}
HgVersionCommand(HgConfig config, ProcessExecutor executor) {
this.config = config;
this.executor = executor;
}
public HgVersion get() {
return new HgVersion(getHgVersion(), getPythonVersion());
}
@Nonnull
private String getPythonVersion() {
try {
String content = exec(config.getPythonBinary(), PYTHON_ARGS);
int index = content.indexOf(' ');
if (index > 0) {
return content.substring(0, index);
}
} catch (IOException ex) {
LOG.warn("failed to get python version", ex);
} catch (InterruptedException ex) {
LOG.warn("failed to get python version", ex);
Thread.currentThread().interrupt();
}
return HgVersion.UNKNOWN;
}
@Nonnull
private String getHgVersion() {
try {
return exec(config.getHgBinary(), HG_ARGS).trim();
} catch (IOException ex) {
LOG.warn("failed to get mercurial version", ex);
} catch (InterruptedException ex) {
LOG.warn("failed to get mercurial version", ex);
Thread.currentThread().interrupt();
}
return HgVersion.UNKNOWN;
}
@SuppressWarnings("UnstableApiUsage")
private String exec(String command, String[] args) throws IOException, InterruptedException {
List<String> cmd = new ArrayList<>();
cmd.add(command);
cmd.addAll(Arrays.asList(args));
Process process = executor.execute(cmd);
byte[] bytes = ByteStreams.toByteArray(process.getInputStream());
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new IOException("process ends with exit code " + exitCode);
}
return new String(bytes, StandardCharsets.UTF_8);
}
@FunctionalInterface
interface ProcessExecutor {
Process execute(List<String> command) throws IOException;
}
}

View File

@@ -36,27 +36,21 @@ import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.work.SimpleWorkingCopyFactory;
import sonia.scm.repository.work.WorkingCopyPool;
import sonia.scm.util.IOUtil;
import sonia.scm.web.HgRepositoryEnvironmentBuilder;
import javax.inject.Inject;
import javax.inject.Provider;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.function.BiConsumer;
public class SimpleHgWorkingCopyFactory extends SimpleWorkingCopyFactory<Repository, Repository, HgCommandContext> implements HgWorkingCopyFactory {
private final Provider<HgRepositoryEnvironmentBuilder> hgRepositoryEnvironmentBuilder;
@Inject
public SimpleHgWorkingCopyFactory(Provider<HgRepositoryEnvironmentBuilder> hgRepositoryEnvironmentBuilder, WorkingCopyPool workdirProvider) {
public SimpleHgWorkingCopyFactory(WorkingCopyPool workdirProvider) {
super(workdirProvider);
this.hgRepositoryEnvironmentBuilder = hgRepositoryEnvironmentBuilder;
}
@Override
public ParentAndClone<Repository, Repository> initialize(HgCommandContext context, File target, String initialBranch) {
Repository centralRepository = openCentral(context);
Repository centralRepository = context.openForWrite();
CloneCommand cloneCommand = CloneCommandFlags.on(centralRepository);
if (initialBranch != null) {
cloneCommand.updaterev(initialBranch);
@@ -76,7 +70,7 @@ public class SimpleHgWorkingCopyFactory extends SimpleWorkingCopyFactory<Reposit
// The hg api to create a command is meant to be used from the command classes, not from their "flags" base classes.
@SuppressWarnings("java:S3252")
protected ParentAndClone<Repository, Repository> reclaim(HgCommandContext context, File target, String initialBranch) throws ReclaimFailedException {
Repository centralRepository = openCentral(context);
Repository centralRepository = context.openForWrite();
try {
BaseRepository clone = Repository.open(target);
for (String unknown : StatusCommand.on(clone).execute().getUnknown()) {
@@ -89,12 +83,6 @@ public class SimpleHgWorkingCopyFactory extends SimpleWorkingCopyFactory<Reposit
}
}
public Repository openCentral(HgCommandContext context) {
BiConsumer<sonia.scm.repository.Repository, Map<String, String>> repositoryMapBiConsumer =
(repository, environment) -> hgRepositoryEnvironmentBuilder.get().buildFor(repository, null, environment);
return context.openWithSpecialEnvironment(repositoryMapBiConsumer);
}
private void delete(File directory, String unknownFile) throws IOException {
IOUtil.delete(new File(directory, unknownFile));
}
@@ -111,7 +99,7 @@ public class SimpleHgWorkingCopyFactory extends SimpleWorkingCopyFactory<Reposit
@Override
public void configure(PullCommand pullCommand) {
pullCommand.cmdAppend("--config", "hooks.changegroup.scm=python:scmhooks.postHook");
pullCommand.cmdAppend("--config", "hooks.pretxnchangegroup.scm=python:scmhooks.preHook");
pullCommand.cmdAppend("--config", "hooks.changegroup.scm=python:scmhooks.post_hook");
pullCommand.cmdAppend("--config", "hooks.pretxnchangegroup.scm=python:scmhooks.pre_hook");
}
}

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.web;
import com.google.common.base.Stopwatch;
@@ -32,6 +32,7 @@ import org.slf4j.LoggerFactory;
import sonia.scm.SCMContext;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgEnvironmentBuilder;
import sonia.scm.repository.HgPythonScript;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.Repository;
@@ -42,29 +43,21 @@ import sonia.scm.web.cgi.CGIExecutor;
import sonia.scm.web.cgi.CGIExecutorFactory;
import sonia.scm.web.cgi.EnvList;
//~--- JDK imports ------------------------------------------------------------
import java.io.File;
import java.io.IOException;
import java.util.Enumeration;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.File;
import java.io.IOException;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*/
@Singleton
public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
{
/** Field description */
public static final String ENV_SESSION_PREFIX = "SCM_";
public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet {
/** Field description */
private static final long serialVersionUID = -3492811300905099810L;
@@ -80,13 +73,13 @@ public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
ScmConfiguration configuration,
HgRepositoryHandler handler,
RepositoryRequestListenerUtil requestListenerUtil,
HgRepositoryEnvironmentBuilder hgRepositoryEnvironmentBuilder)
HgEnvironmentBuilder environmentBuilder)
{
this.cgiExecutorFactory = cgiExecutorFactory;
this.configuration = configuration;
this.handler = handler;
this.requestListenerUtil = requestListenerUtil;
this.hgRepositoryEnvironmentBuilder = hgRepositoryEnvironmentBuilder;
this.environmentBuilder = environmentBuilder;
this.exceptionHandler = new HgCGIExceptionHandler();
this.command = HgPythonScript.HGWEB.getFile(SCMContext.getContext());
}
@@ -108,11 +101,7 @@ public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
{
handleRequest(request, response, repository);
}
catch (ServletException ex)
{
exceptionHandler.handleException(request, response, ex);
}
catch (IOException ex)
catch (ServletException | IOException ex)
{
exceptionHandler.handleException(request, response, ex);
}
@@ -146,29 +135,6 @@ public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
}
}
/**
* Method description
*
*
* @param env
* @param session
*/
@SuppressWarnings("unchecked")
private void passSessionAttributes(EnvList env, HttpSession session)
{
Enumeration<String> enm = session.getAttributeNames();
while (enm.hasMoreElements())
{
String key = enm.nextElement();
if (key.startsWith(ENV_SESSION_PREFIX))
{
env.set(key, session.getAttribute(key).toString());
}
}
}
/**
* Method description
*
@@ -192,7 +158,9 @@ public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
executor.setExceptionHandler(exceptionHandler);
executor.setStatusCodeHandler(exceptionHandler);
executor.setContentLengthWorkaround(true);
hgRepositoryEnvironmentBuilder.buildFor(repository, request, executor.getEnvironment().asMutableMap());
EnvList env = executor.getEnvironment();
environmentBuilder.write(repository).forEach(env::set);
String interpreter = getInterpreter();
@@ -248,5 +216,5 @@ public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
/** Field description */
private final RepositoryRequestListenerUtil requestListenerUtil;
private final HgRepositoryEnvironmentBuilder hgRepositoryEnvironmentBuilder;
private final HgEnvironmentBuilder environmentBuilder;
}

View File

@@ -1,446 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.web;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.io.Closeables;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.NotFoundException;
import sonia.scm.repository.HgContext;
import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.RepositoryHookType;
import sonia.scm.repository.api.HgHookMessage;
import sonia.scm.repository.api.HgHookMessage.Severity;
import sonia.scm.repository.spi.HgHookContextProvider;
import sonia.scm.repository.spi.HookEventFacade;
import sonia.scm.security.BearerToken;
import sonia.scm.security.CipherUtil;
import sonia.scm.util.HttpUtil;
import sonia.scm.util.Util;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*/
@Singleton
public class HgHookCallbackServlet extends HttpServlet
{
/** Field description */
public static final String HGHOOK_POST_RECEIVE = "changegroup";
/** Field description */
public static final String HGHOOK_PRE_RECEIVE = "pretxnchangegroup";
/** Field description */
public static final String PARAM_REPOSITORYID = "repositoryId";
/** Field description */
private static final String PARAM_CHALLENGE = "challenge";
/** Field description */
private static final String PARAM_TOKEN = "token";
/** Field description */
private static final String PARAM_NODE = "node";
/** Field description */
private static final String PARAM_PING = "ping";
/** Field description */
private static final Pattern REGEX_URL =
Pattern.compile("^/hook/hg/([^/]+)$");
/** the logger for HgHookCallbackServlet */
private static final Logger logger =
LoggerFactory.getLogger(HgHookCallbackServlet.class);
/** Field description */
private static final long serialVersionUID = 3531596724828189353L;
//~--- constructors ---------------------------------------------------------
@Inject
public HgHookCallbackServlet(HookEventFacade hookEventFacade,
HgRepositoryHandler handler, HgHookManager hookManager,
Provider<HgContext> contextProvider)
{
this.hookEventFacade = hookEventFacade;
this.handler = handler;
this.hookManager = hookManager;
this.contextProvider = contextProvider;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param request
* @param response
*
* @throws IOException
* @throws ServletException
*/
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
{
String ping = request.getParameter(PARAM_PING);
if (Util.isNotEmpty(ping) && Boolean.parseBoolean(ping))
{
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
}
else
{
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
}
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try {
handlePostRequest(request, response);
} catch (IOException ex) {
logger.warn("error in hook callback execution, sending internal server error", ex);
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
private void handlePostRequest(HttpServletRequest request, HttpServletResponse response) throws IOException
{
String strippedURI = HttpUtil.getStrippedURI(request);
Matcher m = REGEX_URL.matcher(strippedURI);
if (m.matches())
{
String repositoryId = getRepositoryId(request);
String type = m.group(1);
String challenge = request.getParameter(PARAM_CHALLENGE);
if (Util.isNotEmpty(challenge))
{
String node = request.getParameter(PARAM_NODE);
if (Util.isNotEmpty(node))
{
String token = request.getParameter(PARAM_TOKEN);
if (Util.isNotEmpty(token))
{
authenticate(token);
}
hookCallback(response, type, repositoryId, challenge, node);
}
else if (logger.isDebugEnabled())
{
logger.debug("node parameter not found");
}
}
else if (logger.isDebugEnabled())
{
logger.debug("challenge parameter not found");
}
}
else
{
if (logger.isDebugEnabled())
{
logger.debug("url does not match");
}
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
}
}
private void authenticate(String token)
{
try
{
token = CipherUtil.getInstance().decode(token);
if (Util.isNotEmpty(token))
{
Subject subject = SecurityUtils.getSubject();
AuthenticationToken accessToken = createToken(token);
//J-
subject.login(accessToken);
}
}
catch (Exception ex)
{
logger.error("could not authenticate user", ex);
}
}
private AuthenticationToken createToken(String tokenString)
{
return BearerToken.valueOf(tokenString);
}
private void fireHook(HttpServletResponse response, String repositoryId, String node, RepositoryHookType type)
throws IOException
{
HgHookContextProvider context = null;
try
{
if (type == RepositoryHookType.PRE_RECEIVE)
{
contextProvider.get().setPending(true);
}
File repositoryDirectory = handler.getDirectory(repositoryId);
context = new HgHookContextProvider(handler, repositoryDirectory, hookManager,
node, type);
hookEventFacade.handle(repositoryId).fireHookEvent(type, context);
printMessages(response, context);
}
catch (NotFoundException ex)
{
logger.error(ex.getMessage());
logger.trace("repository not found", ex);
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
catch (Exception ex)
{
sendError(response, context, ex);
}
}
private void hookCallback(HttpServletResponse response, String typeName, String repositoryId, String challenge, String node) throws IOException {
if (hookManager.isAcceptAble(challenge))
{
RepositoryHookType type = null;
if (HGHOOK_PRE_RECEIVE.equals(typeName))
{
type = RepositoryHookType.PRE_RECEIVE;
}
else if (HGHOOK_POST_RECEIVE.equals(typeName))
{
type = RepositoryHookType.POST_RECEIVE;
}
if (type != null)
{
fireHook(response, repositoryId, node, type);
}
else
{
if (logger.isWarnEnabled())
{
logger.warn("unknown hook type {}", typeName);
}
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
}
}
else
{
if (logger.isWarnEnabled())
{
logger.warn("hg hook challenge is not accept able");
}
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
}
}
/**
* Method description
*
*
* @param writer
* @param msg
*/
private void printMessage(PrintWriter writer, HgHookMessage msg)
{
writer.append('_');
if (msg.getSeverity() == Severity.ERROR)
{
writer.append("e[SCM] Error: ");
}
else
{
writer.append("n[SCM] ");
}
writer.println(msg.getMessage());
}
/**
* Method description
*
*
* @param response
* @param context
*
* @throws IOException
*/
private void printMessages(HttpServletResponse response,
HgHookContextProvider context)
throws IOException
{
List<HgHookMessage> msgs = context.getHgMessageProvider().getMessages();
if (Util.isNotEmpty(msgs))
{
PrintWriter writer = null;
try
{
writer = response.getWriter();
printMessages(writer, msgs);
}
finally
{
Closeables.close(writer, false);
}
}
}
/**
* Method description
*
*
* @param writer
* @param msgs
*/
private void printMessages(PrintWriter writer, List<HgHookMessage> msgs)
{
for (HgHookMessage msg : msgs)
{
printMessage(writer, msg);
}
}
/**
* Method description
*
*
* @param response
* @param context
* @param ex
*
* @throws IOException
*/
private void sendError(HttpServletResponse response,
HgHookContextProvider context, Exception ex)
throws IOException
{
logger.warn("hook ended with exception", ex);
response.setStatus(HttpServletResponse.SC_CONFLICT);
String msg = ex.getMessage();
List<HgHookMessage> msgs = null;
if (context != null)
{
msgs = context.getHgMessageProvider().getMessages();
}
if (!Strings.isNullOrEmpty(msg) || Util.isNotEmpty(msgs))
{
PrintWriter writer = null;
try
{
writer = response.getWriter();
if (Util.isNotEmpty(msgs))
{
printMessages(writer, msgs);
}
if (!Strings.isNullOrEmpty(msg))
{
printMessage(writer, new HgHookMessage(Severity.ERROR, msg));
}
}
finally
{
Closeables.close(writer, true);
}
}
}
//~--- get methods ----------------------------------------------------------
private String getRepositoryId(HttpServletRequest request)
{
String id = request.getParameter(PARAM_REPOSITORYID);
Preconditions.checkArgument(!Strings.isNullOrEmpty(id), "repository id not found in request");
return id;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private final Provider<HgContext> contextProvider;
/** Field description */
private final HgRepositoryHandler handler;
/** Field description */
private final HookEventFacade hookEventFacade;
/** Field description */
private final HgHookManager hookManager;
}

View File

@@ -1,80 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.web;
import sonia.scm.repository.HgEnvironment;
import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.Repository;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.util.Map;
public class HgRepositoryEnvironmentBuilder {
private static final String ENV_REPOSITORY_NAME = "REPO_NAME";
private static final String ENV_REPOSITORY_PATH = "SCM_REPOSITORY_PATH";
private static final String ENV_REPOSITORY_ID = "SCM_REPOSITORY_ID";
private static final String ENV_PYTHON_HTTPS_VERIFY = "PYTHONHTTPSVERIFY";
private static final String ENV_HTTP_POST_ARGS = "SCM_HTTP_POST_ARGS";
private final HgRepositoryHandler handler;
private final HgHookManager hookManager;
@Inject
public HgRepositoryEnvironmentBuilder(HgRepositoryHandler handler, HgHookManager hookManager) {
this.handler = handler;
this.hookManager = hookManager;
}
public void buildFor(Repository repository, HttpServletRequest request, Map<String, String> environment) {
File directory = handler.getDirectory(repository.getId());
environment.put(ENV_REPOSITORY_NAME, repository.getNamespace() + "/" + repository.getName());
environment.put(ENV_REPOSITORY_ID, repository.getId());
environment.put(ENV_REPOSITORY_PATH,
directory.getAbsolutePath());
// add hook environment
if (handler.getConfig().isDisableHookSSLValidation()) {
// disable ssl validation
// Issue 959: https://goo.gl/zH5eY8
environment.put(ENV_PYTHON_HTTPS_VERIFY, "0");
}
// enable experimental httppostargs protocol of mercurial
// Issue 970: https://goo.gl/poascp
environment.put(ENV_HTTP_POST_ARGS, String.valueOf(handler.getConfig().isEnableHttpPostArgs()));
HgEnvironment.prepareEnvironment(
environment,
handler,
hookManager,
request
);
}
}

View File

@@ -34,9 +34,6 @@ import sonia.scm.api.v2.resources.HgConfigPackagesToDtoMapper;
import sonia.scm.api.v2.resources.HgConfigToHgConfigDtoMapper;
import sonia.scm.installer.HgPackageReader;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.HgContext;
import sonia.scm.repository.HgContextProvider;
import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.spi.HgWorkingCopyFactory;
import sonia.scm.repository.spi.SimpleHgWorkingCopyFactory;
@@ -45,26 +42,10 @@ import sonia.scm.repository.spi.SimpleHgWorkingCopyFactory;
* @author Sebastian Sdorra
*/
@Extension
public class HgServletModule extends ServletModule
{
public class HgServletModule extends ServletModule {
/** Field description */
public static final String MAPPING_HG = "/hg/*";
/** Field description */
public static final String MAPPING_HOOK = "/hook/hg/*";
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*/
@Override
protected void configureServlets()
{
bind(HgContext.class).toProvider(HgContextProvider.class);
bind(HgHookManager.class);
protected void configureServlets() {
bind(HgPackageReader.class);
bind(HgConfigDtoToHgConfigMapper.class).to(Mappers.getMapper(HgConfigDtoToHgConfigMapper.class).getClass());
@@ -72,9 +53,6 @@ public class HgServletModule extends ServletModule
bind(HgConfigPackagesToDtoMapper.class).to(Mappers.getMapper(HgConfigPackagesToDtoMapper.class).getClass());
bind(HgConfigInstallationsToDtoMapper.class);
// bind servlets
serve(MAPPING_HOOK).with(HgHookCallbackServlet.class);
bind(HgWorkingCopyFactory.class).to(SimpleHgWorkingCopyFactory.class);
}
}

View File

@@ -21,189 +21,48 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.web;
//~--- non-JDK imports --------------------------------------------------------
import com.aragost.javahg.Repository;
import com.aragost.javahg.RepositoryConfiguration;
import com.google.common.base.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContext;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgEnvironment;
import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.HgPythonScript;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.spi.javahg.HgFileviewExtension;
import sonia.scm.util.HttpUtil;
import sonia.scm.util.Util;
//~--- JDK imports ------------------------------------------------------------
import java.io.File;
import java.nio.charset.Charset;
import java.util.Map;
import java.util.function.Consumer;
import javax.servlet.http.HttpServletRequest;
/**
*
* @author Sebastian Sdorra
*/
public final class HgUtil
{
public final class HgUtil {
/** Field description */
public static final String REVISION_TIP = "tip";
/** Field description */
private static final String USERAGENT_HG = "mercurial/";
/**
* the logger for HgUtil
*/
private static final Logger logger = LoggerFactory.getLogger(HgUtil.class);
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*/
private HgUtil() {}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param handler
* @param hookManager
* @param directory
* @param encoding
* @param pending
*
* @return
*/
public static Repository open(HgRepositoryHandler handler,
HgHookManager hookManager, File directory, String encoding, boolean pending)
{
return open(
handler,
directory,
encoding,
pending,
environment -> HgEnvironment.prepareEnvironment(environment, handler, hookManager)
);
}
public static Repository open(HgRepositoryHandler handler,
File directory, String encoding, boolean pending,
Consumer<Map<String, String>> prepareEnvironment)
{
String enc = encoding;
if (Strings.isNullOrEmpty(enc))
{
enc = handler.getConfig().getEncoding();
}
RepositoryConfiguration repoConfiguration = RepositoryConfiguration.DEFAULT;
prepareEnvironment.accept(repoConfiguration.getEnvironment());
repoConfiguration.addExtension(HgFileviewExtension.class);
repoConfiguration.setEnablePendingChangesets(pending);
try
{
Charset charset = Charset.forName(enc);
logger.trace("set encoding {} for mercurial", enc);
repoConfiguration.setEncoding(charset);
}
catch (IllegalArgumentException ex)
{
logger.error("could not set encoding for mercurial", ex);
}
repoConfiguration.setHgBin(handler.getConfig().getHgBinary());
logger.debug("open hg repository {}: encoding: {}, pending: {}", directory, enc, pending);
return Repository.open(repoConfiguration, directory);
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @param config
*
* @return
*/
public static String getPythonPath(HgConfig config)
{
public static String getPythonPath(HgConfig config) {
String pythonPath = Util.EMPTY_STRING;
if (config != null)
{
if (config != null) {
pythonPath = Util.nonNull(config.getPythonPath());
}
if (Util.isNotEmpty(pythonPath))
{
if (Util.isNotEmpty(pythonPath)) {
pythonPath = pythonPath.concat(File.pathSeparator);
}
//J-
pythonPath = pythonPath.concat(
HgPythonScript.getScriptDirectory(
SCMContext.getContext()
).getAbsolutePath()
);
//J+
return pythonPath;
}
/**
* Method description
*
*
* @param revision
*
* @return
*/
public static String getRevision(String revision)
{
return Util.isEmpty(revision)
? REVISION_TIP
: revision;
public static String getRevision(String revision) {
return Util.isEmpty(revision) ? REVISION_TIP : revision;
}
/**
* Returns true if the request comes from a mercurial client.
*
*
* @param request servlet request
*
* @return true if the client is mercurial
*/
public static boolean isHgClient(HttpServletRequest request)
{
return HttpUtil.userAgentStartsWith(request, USERAGENT_HG);
}
}

View File

@@ -33,7 +33,6 @@ type Configuration = {
encoding: string;
useOptimizedBytecode: boolean;
showRevisionInId: boolean;
disableHookSSLValidation: boolean;
enableHttpPostArgs: boolean;
_links: Links;
};
@@ -139,7 +138,6 @@ class HgConfigurationForm extends React.Component<Props, State> {
{this.checkbox("showRevisionInId")}
</div>
<div className="column is-half">
{this.checkbox("disableHookSSLValidation")}
{this.checkbox("enableHttpPostArgs")}
</div>
</div>

View File

@@ -25,8 +25,6 @@
"showRevisionInIdHelpText": "Die Revision als Teil der Node ID anzeigen.",
"enableHttpPostArgs": "HttpPostArgs Protocol aktivieren",
"enableHttpPostArgsHelpText": "Aktiviert das experimentelle HttpPostArgs Protokoll von Mercurial. Das HttpPostArgs Protokoll verwendet den Post Request Body anstatt des HTTP Headers um Meta Informationen zu versenden. Dieses Vorgehen reduziert die Header Größe der Mercurial Requests. HttpPostArgs wird seit Mercurial 3.8 unterstützt.",
"disableHookSSLValidation": "SSL Validierung für Hooks deaktivieren",
"disableHookSSLValidationHelpText": "Deaktiviert die Validierung von SSL Zertifikaten für den Mercurial Hook, der die Repositoryänderungen wieder zurück an den SCM-Manager leitet. Diese Option sollte nur benutzt werden, wenn der SCM-Manager ein selbstsigniertes Zertifikat verwendet.",
"disabled": "Deaktiviert",
"disabledHelpText": "Aktiviert oder deaktiviert das Mercurial Plugin.",
"required": "Dieser Konfigurationswert wird benötigt"

View File

@@ -25,8 +25,6 @@
"showRevisionInIdHelpText": "Show revision as part of the node id.",
"enableHttpPostArgs": "Enable HttpPostArgs Protocol",
"enableHttpPostArgsHelpText": "Enables the experimental HttpPostArgs Protocol of mercurial. The HttpPostArgs Protocol uses the body of post requests to send the meta information instead of http headers. This helps to reduce the header size of mercurial requests. HttpPostArgs is supported since mercurial 3.8.",
"disableHookSSLValidation": "Disable SSL Validation on Hooks",
"disableHookSSLValidationHelpText": "Disables the validation of ssl certificates for the mercurial hook, which forwards the repository changes back to scm-manager. This option should only be used, if SCM-Manager uses a self signed certificate.",
"disabled": "Disabled",
"disabledHelpText": "Enable or disable the Mercurial plugin.",
"required": "This configuration value is required"

View File

@@ -39,8 +39,8 @@ u.setconfig(b'web', b'push_ssl', b'false')
u.setconfig(b'web', b'allow_read', b'*')
u.setconfig(b'web', b'allow_push', b'*')
u.setconfig(b'hooks', b'changegroup.scm', b'python:scmhooks.postHook')
u.setconfig(b'hooks', b'pretxnchangegroup.scm', b'python:scmhooks.preHook')
u.setconfig(b'hooks', b'changegroup.scm', b'python:scmhooks.post_hook')
u.setconfig(b'hooks', b'pretxnchangegroup.scm', b'python:scmhooks.pre_hook')
# pass SCM_HTTP_POST_ARGS to enable experimental httppostargs protocol of mercurial
# SCM_HTTP_POST_ARGS is set by HgCGIServlet

View File

@@ -29,112 +29,84 @@
# changegroup.scm = python:scmhooks.callback
#
import os, sys
client = None
# compatibility layer between python 2 and 3 urllib implementations
if sys.version_info[0] < 3:
import urllib, urllib2
# create type alias for url error
URLError = urllib2.URLError
class Python2Client:
def post(self, url, values):
data = urllib.urlencode(values)
# open url but ignore proxy settings
proxy_handler = urllib2.ProxyHandler({})
opener = urllib2.build_opener(proxy_handler)
req = urllib2.Request(url, data)
req.add_header("X-XSRF-Token", xsrf)
return opener.open(req)
client = Python2Client()
else:
import urllib.parse, urllib.request, urllib.error
# create type alias for url error
URLError = urllib.error.URLError
class Python3Client:
def post(self, url, values):
data = urllib.parse.urlencode(values)
# open url but ignore proxy settings
proxy_handler = urllib.request.ProxyHandler({})
opener = urllib.request.build_opener(proxy_handler)
req = urllib.request.Request(url, data.encode())
req.add_header("X-XSRF-Token", xsrf)
return opener.open(req)
client = Python3Client()
import os, sys, json, socket, struct
# read environment
baseUrl = os.environ['SCM_URL']
port = os.environ['SCM_HOOK_PORT']
challenge = os.environ['SCM_CHALLENGE']
token = os.environ['SCM_BEARER_TOKEN']
xsrf = os.environ['SCM_XSRF']
repositoryId = os.environ['SCM_REPOSITORY_ID']
transactionId = os.environ['SCM_TRANSACTION_ID']
def printMessages(ui, msgs):
for raw in msgs:
line = raw
if hasattr(line, "encode"):
line = line.encode()
if line.startswith(b"_e") or line.startswith(b"_n"):
line = line[2:]
ui.warn(b'%s\n' % line.rstrip())
def print_messages(ui, messages):
for message in messages:
msg = "[SCM]"
if message['severity'] == "ERROR":
msg += " Error"
msg += ": " + message['message'] + "\n"
ui.warn(msg.encode('utf-8'))
def callHookUrl(ui, repo, hooktype, node):
def read_bytes(connection, length):
received = bytearray()
while len(received) < length:
buffer = connection.recv(length - len(received))
received = received + buffer
return received
def read_int(connection):
data = read_bytes(connection, 4)
return struct.unpack('>i', bytearray(data))[0]
def fire_hook(ui, repo, hooktype, node):
abort = True
ui.debug( b"send scm-hook for " + node + b"\n" )
connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
url = baseUrl + hooktype.decode("utf-8")
ui.debug( b"send scm-hook to " + url.encode() + b" and " + node + b"\n" )
values = {'node': node.decode("utf-8"), 'challenge': challenge, 'token': token, 'repositoryPath': repo.root, 'repositoryId': repositoryId}
conn = client.post(url, values)
if 200 <= conn.code < 300:
ui.debug( b"scm-hook " + hooktype + b" success with status code " + str(conn.code).encode() + b"\n" )
printMessages(ui, conn)
abort = False
else:
ui.warn( b"ERROR: scm-hook failed with error code " + str(conn.code).encode() + b"\n" )
except URLError as e:
msg = None
# some URLErrors have no read method
if hasattr(e, "read"):
msg = e.read()
elif hasattr(e, "code"):
msg = "scm-hook failed with error code " + e.code + "\n"
else:
msg = str(e)
if len(msg) > 0:
printMessages(ui, msg.splitlines(True))
else:
ui.warn( b"ERROR: scm-hook failed with an unknown error\n" )
ui.traceback()
values = {'token': token, 'type': hooktype, 'repositoryId': repositoryId, 'transactionId': transactionId, 'challenge': challenge, 'node': node.decode('utf8') }
connection.connect(("127.0.0.1", int(port)))
data = json.dumps(values).encode('utf-8')
connection.send(struct.pack('>i', len(data)))
connection.sendall(data)
length = read_int(connection)
if length > 8192:
ui.warn( b"scm-hook received message which exceeds the limit of 8192\n" )
return True
d = read_bytes(connection, length)
response = json.loads(d.decode("utf-8"))
abort = response['abort']
print_messages(ui, response['messages'])
except ValueError:
ui.warn( b"scm-hook failed with an exception\n" )
ui.traceback()
finally:
connection.close()
return abort
def callback(ui, repo, hooktype, node=None):
abort = True
if node != None:
if len(baseUrl) > 0:
abort = callHookUrl(ui, repo, hooktype, node)
if len(port) > 0:
abort = fire_hook(ui, repo, hooktype, node)
else:
ui.warn(b"ERROR: scm-manager hooks are disabled, please check your configuration and the scm-manager log for details\n")
abort = False
else:
ui.warn(b"changeset node is not available")
return abort
def preHook(ui, repo, hooktype, node=None, source=None, pending=None, **kwargs):
def pre_hook(ui, repo, hooktype, node=None, source=None, pending=None, **kwargs):
# older mercurial versions
if pending != None:
pending()
# newer mercurial version
# we have to make in-memory changes visible to external process
# this does not happen automatically, because mercurial treat our hooks as internal hooks
# this does not happen automatically, because mercurial treat our hooks as internal hook
# see hook.py at mercurial sources _exthook
try:
if repo is not None:
@@ -143,10 +115,10 @@ def preHook(ui, repo, hooktype, node=None, source=None, pending=None, **kwargs):
if tr and not tr.writepending():
ui.warn(b"no pending write transaction found")
except AttributeError:
ui.debug(b"mercurial does not support currenttransation")
ui.debug(b"mercurial does not support currenttransaction")
# do nothing
return callback(ui, repo, hooktype, node)
return callback(ui, repo, "PRE_RECEIVE", node)
def postHook(ui, repo, hooktype, node=None, source=None, pending=None, **kwargs):
return callback(ui, repo, hooktype, node)
def post_hook(ui, repo, hooktype, node=None, source=None, pending=None, **kwargs):
return callback(ui, repo, "POST_RECEIVE", node)

View File

@@ -1,45 +0,0 @@
#
# MIT License
#
# Copyright (c) 2020-present Cloudogu GmbH and Contributors
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
import sys
from mercurial import util
from xml.dom.minidom import Document
pyVersion = sys.version_info
pyVersion = str(pyVersion.major) + "." + str(pyVersion.minor) + "." + str(pyVersion.micro)
hgVersion = util.version()
doc = Document()
root = doc.createElement('version')
pyNode = doc.createElement('python')
pyNode.appendChild(doc.createTextNode(pyVersion))
root.appendChild(pyNode)
hgNode = doc.createElement('mercurial')
hgNode.appendChild(doc.createTextNode(hgVersion))
root.appendChild(hgNode)
doc.appendChild(root)
doc.writexml(sys.stdout, encoding='UTF-8')

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import org.junit.Test;
@@ -52,7 +52,6 @@ public class HgConfigDtoToHgConfigMapperTest {
assertEquals("/etc/", config.getPythonPath());
assertTrue(config.isShowRevisionInId());
assertTrue(config.isUseOptimizedBytecode());
assertTrue(config.isDisableHookSSLValidation());
assertTrue(config.isEnableHttpPostArgs());
}
@@ -65,7 +64,6 @@ public class HgConfigDtoToHgConfigMapperTest {
configDto.setPythonPath("/etc/");
configDto.setShowRevisionInId(true);
configDto.setUseOptimizedBytecode(true);
configDto.setDisableHookSSLValidation(true);
configDto.setEnableHttpPostArgs(true);
return configDto;

View File

@@ -0,0 +1,159 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.SCMContext;
import sonia.scm.TransactionId;
import sonia.scm.repository.hooks.HookEnvironment;
import sonia.scm.repository.hooks.HookServer;
import sonia.scm.security.AccessToken;
import sonia.scm.security.AccessTokenBuilderFactory;
import sonia.scm.security.CipherUtil;
import sonia.scm.security.Xsrf;
import javax.annotation.Nonnull;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Map;
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static sonia.scm.repository.DefaultHgEnvironmentBuilder.*;
@ExtendWith(MockitoExtension.class)
class DefaultHgEnvironmentBuilderTest {
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private AccessTokenBuilderFactory accessTokenBuilderFactory;
@Mock
private HgRepositoryHandler repositoryHandler;
@Mock
private HookEnvironment hookEnvironment;
@Mock
private HookServer server;
@InjectMocks
private DefaultHgEnvironmentBuilder builder;
private Path directory;
@BeforeEach
void setBaseDir(@TempDir Path directory) {
this.directory = directory;
TempSCMContextProvider context = (TempSCMContextProvider) SCMContext.getContext();
context.setBaseDirectory(directory.resolve("home").toFile());
}
@Test
void shouldReturnReadEnvironment() {
Repository heartOfGold = prepareForRead("/usr/lib/python", "42");
Map<String, String> env = builder.read(heartOfGold);
assertReadEnv(env, "/usr/lib/python", "42");
}
@Test
void shouldReturnWriteEnvironment() throws IOException {
Repository heartOfGold = prepareForWrite("/opt/python", "21");
Map<String, String> env = builder.write(heartOfGold);
assertReadEnv(env, "/opt/python", "21");
String bearer = CipherUtil.getInstance().decode(env.get(ENV_BEARER_TOKEN));
assertThat(bearer).isEqualTo("secretAC");
assertThat(env)
.containsEntry(ENV_CHALLENGE, "challenge")
.containsEntry(ENV_HOOK_PORT, "2042");
}
@Test
void shouldSetTransactionId() throws IOException {
TransactionId.set("ti42");
Repository heartOfGold = prepareForWrite("/opt/python", "21");
Map<String, String> env = builder.write(heartOfGold);
assertThat(env).containsEntry(ENV_TRANSACTION_ID, "ti42");
}
@Test
void shouldThrowIllegalStateIfServerCouldNotBeStarted() throws IOException {
when(server.start()).thenThrow(new IOException("failed to start"));
Repository repository = prepareForRead("/usr", "42");
assertThrows(IllegalStateException.class, () -> builder.write(repository));
}
private Repository prepareForWrite(String pythonPath, String id) throws IOException {
Repository heartOfGold = prepareForRead(pythonPath, id);
applyAccessToken("secretAC");
when(server.start()).thenReturn(2042);
when(hookEnvironment.getChallenge()).thenReturn("challenge");
return heartOfGold;
}
private void applyAccessToken(String compact) {
AccessToken accessToken = mock(AccessToken.class);
when(accessTokenBuilderFactory.create().custom(Xsrf.TOKEN_KEY, null).build()).thenReturn(accessToken);
when(accessToken.compact()).thenReturn(compact);
}
private void assertReadEnv(Map<String, String> env, String pythonPath, String repositoryId) {
assertThat(env)
.containsEntry(ENV_REPOSITORY_ID, repositoryId)
.containsEntry(ENV_REPOSITORY_NAME, "hitchhiker/HeartOfGold")
.containsEntry(ENV_HTTP_POST_ARGS, "false")
.containsEntry(ENV_REPOSITORY_PATH, directory.resolve("repo").toAbsolutePath().toString())
.containsEntry(ENV_PYTHON_PATH, pythonPath + File.pathSeparator + directory.resolve("home/lib/python"));
}
@Nonnull
private Repository prepareForRead(String pythonPath, String id) {
when(repositoryHandler.getDirectory(id)).thenReturn(directory.resolve("repo").toFile());
HgConfig config = new HgConfig();
config.setPythonPath(pythonPath);
when(repositoryHandler.getConfig()).thenReturn(config);
Repository heartOfGold = RepositoryTestData.createHeartOfGold();
heartOfGold.setId(id);
return heartOfGold;
}
}

View File

@@ -21,30 +21,20 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
//~--- JDK imports ------------------------------------------------------------
import java.util.Collections;
import java.util.Map;
import java.io.File;
import java.io.IOException;
/**
*
* @author Sebastian Sdorra
*/
public class HgVersionHandler extends AbstractHgHandler
{
public HgVersionHandler(HgRepositoryHandler handler, HgContext context,
File directory)
{
super(handler, context, null, directory);
public class EmptyHgEnvironmentBuilder implements HgEnvironmentBuilder {
@Override
public Map<String, String> read(Repository repository) {
return Collections.emptyMap();
}
//~--- get methods ----------------------------------------------------------
public HgVersion getVersion() throws IOException {
return getResultFromScript(HgVersion.class, HgPythonScript.VERSION);
@Override
public Map<String, String> write(Repository repository) {
return Collections.emptyMap();
}
}

View File

@@ -1,111 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.OutOfScopeException;
import com.google.inject.Provider;
import com.google.inject.ProvisionException;
import com.google.inject.Scope;
import com.google.inject.servlet.RequestScoped;
import com.google.inject.util.Providers;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class HgContextProviderTest {
@Mock
private Scope scope;
@Test
void shouldThrowNonOutOfScopeProvisionExceptions() {
Provider<HgContextRequestStore> provider = () -> {
throw new RuntimeException("something different");
};
when(scope.scope(any(Key.class), any(Provider.class))).thenReturn(provider);
Injector injector = Guice.createInjector(new HgContextModule(scope));
assertThrows(ProvisionException.class, () -> injector.getInstance(HgContext.class));
}
@Test
void shouldCreateANewInstanceIfOutOfRequestScope() {
Provider<HgContextRequestStore> provider = () -> {
throw new OutOfScopeException("no request");
};
when(scope.scope(any(Key.class), any(Provider.class))).thenReturn(provider);
Injector injector = Guice.createInjector(new HgContextModule(scope));
HgContext contextOne = injector.getInstance(HgContext.class);
HgContext contextTwo = injector.getInstance(HgContext.class);
assertThat(contextOne).isNotSameAs(contextTwo);
}
@Test
void shouldInjectFromRequestScope() {
HgContextRequestStore requestStore = new HgContextRequestStore();
Provider<HgContextRequestStore> provider = Providers.of(requestStore);
when(scope.scope(any(Key.class), any(Provider.class))).thenReturn(provider);
Injector injector = Guice.createInjector(new HgContextModule(scope));
HgContext contextOne = injector.getInstance(HgContext.class);
HgContext contextTwo = injector.getInstance(HgContext.class);
assertThat(contextOne).isSameAs(contextTwo);
}
private static class HgContextModule extends AbstractModule {
private Scope scope;
private HgContextModule(Scope scope) {
this.scope = scope;
}
@Override
protected void configure() {
bindScope(RequestScoped.class, scope);
bind(HgContextRequestStore.class);
bind(HgContext.class).toProvider(HgContextProvider.class);
}
}
}

View File

@@ -0,0 +1,125 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import com.aragost.javahg.Repository;
import com.google.common.collect.ImmutableMap;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.repository.hooks.HookEnvironment;
import javax.annotation.Nonnull;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class HgRepositoryFactoryTest {
private HgRepositoryHandler handler;
@Mock
private HookEnvironment hookEnvironment;
@Mock
private HgEnvironmentBuilder environmentBuilder;
private HgRepositoryFactory factory;
private sonia.scm.repository.Repository heartOfGold;
@BeforeEach
void setUpFactory(@TempDir Path directory) {
handler = HgTestUtil.createHandler(directory.toFile());
assumeTrue(handler.isConfigured());
factory = new HgRepositoryFactory(handler, hookEnvironment, environmentBuilder);
heartOfGold = createRepository();
}
@Test
void shouldOpenRepositoryForRead() {
Repository repository = factory.openForRead(heartOfGold);
assertThat(repository).isNotNull();
verify(environmentBuilder).read(heartOfGold);
}
@Test
void shouldOpenRepositoryForWrite() {
Repository repository = factory.openForWrite(heartOfGold);
assertThat(repository).isNotNull();
verify(environmentBuilder).write(heartOfGold);
}
@Test
void shouldFallbackToUTF8OnUnknownEncoding() {
handler.getConfig().setEncoding("unknown");
Repository repository = factory.openForRead(heartOfGold);
assertThat(repository.getBaseRepository().getConfiguration().getEncoding()).isEqualTo(StandardCharsets.UTF_8);
}
@Test
void shouldSetPendingChangesetState() {
when(hookEnvironment.isPending()).thenReturn(true);
Repository repository = factory.openForRead(heartOfGold);
assertThat(repository.getBaseRepository().getConfiguration().isEnablePendingChangesets())
.isTrue();
}
@Test
void shouldPassEnvironment() {
when(environmentBuilder.read(heartOfGold)).thenReturn(ImmutableMap.of("spaceship", "heartOfGold"));
Repository repository = factory.openForRead(heartOfGold);
assertThat(repository.getBaseRepository().getConfiguration().getEnvironment())
.containsEntry("spaceship", "heartOfGold");
}
@Nonnull
private sonia.scm.repository.Repository createRepository() {
sonia.scm.repository.Repository heartOfGold = RepositoryTestData.createHeartOfGold("hg");
heartOfGold.setId("42");
handler.create(heartOfGold);
return heartOfGold;
}
}

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
@@ -32,13 +32,17 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.plugin.PluginLoader;
import sonia.scm.repository.spi.HgVersionCommand;
import sonia.scm.store.ConfigurationStoreFactory;
import java.io.File;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
//~--- JDK imports ------------------------------------------------------------
@@ -52,9 +56,6 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
@Mock
private ConfigurationStoreFactory factory;
@Mock
private com.google.inject.Provider<HgContext> provider;
@Override
protected void checkDirectory(File directory) {
File hgDirectory = new File(directory, ".hg");
@@ -70,7 +71,7 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
@Override
protected RepositoryHandler createRepositoryHandler(ConfigurationStoreFactory factory, RepositoryLocationResolver locationResolver, File directory) {
HgRepositoryHandler handler = new HgRepositoryHandler(factory, new HgContextProvider(), locationResolver, null, null);
HgRepositoryHandler handler = new HgRepositoryHandler(factory, locationResolver, null, null);
handler.init(contextProvider);
HgTestUtil.checkForSkip(handler);
@@ -80,7 +81,7 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
@Test
public void getDirectory() {
HgRepositoryHandler repositoryHandler = new HgRepositoryHandler(factory, provider, locationResolver, null, null);
HgRepositoryHandler repositoryHandler = new HgRepositoryHandler(factory, locationResolver, null, null);
HgConfig hgConfig = new HgConfig();
hgConfig.setHgBinary("hg");
@@ -91,4 +92,20 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
File path = repositoryHandler.getDirectory(repository.getId());
assertEquals(repoPath.toString() + File.separator + RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath());
}
@Test
public void shouldReturnVersionInformation() {
PluginLoader pluginLoader = mock(PluginLoader.class);
when(pluginLoader.getUberClassLoader()).thenReturn(HgRepositoryHandler.class.getClassLoader());
HgVersionCommand versionCommand = mock(HgVersionCommand.class);
when(versionCommand.get()).thenReturn(new HgVersion("5.2.0", "3.7.2"));
HgRepositoryHandler handler = new HgRepositoryHandler(
factory, locationResolver, pluginLoader, null
);
String versionInformation = handler.getVersionInformation(versionCommand);
assertThat(versionInformation).startsWith("scm-hg-version/").endsWith("python/3.7.2 mercurial/5.2.0");
}
}

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
@@ -29,16 +29,13 @@ package sonia.scm.repository;
import org.junit.Assume;
import sonia.scm.SCMContext;
import sonia.scm.TempDirRepositoryLocationResolver;
import sonia.scm.security.AccessToken;
import sonia.scm.repository.hooks.HookEnvironment;
import sonia.scm.store.InMemoryConfigurationStoreFactory;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.nio.file.Path;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
//~--- JDK imports ------------------------------------------------------------
@@ -80,49 +77,26 @@ public final class HgTestUtil
}
}
/**
* Method description
*
*
* @param directory
*
* @return
*/
public static HgRepositoryHandler createHandler(File directory) {
TempSCMContextProvider context =
(TempSCMContextProvider) SCMContext.getContext();
public static HgRepositoryHandler createHandler(File directory) {
TempSCMContextProvider context = (TempSCMContextProvider) SCMContext.getContext();
context.setBaseDirectory(directory);
RepositoryDAO repoDao = mock(RepositoryDAO.class);
RepositoryLocationResolver repositoryLocationResolver = new TempDirRepositoryLocationResolver(directory);
HgRepositoryHandler handler =
new HgRepositoryHandler(new InMemoryConfigurationStoreFactory(), new HgContextProvider(), repositoryLocationResolver, null, null);
HgRepositoryHandler handler = new HgRepositoryHandler(
new InMemoryConfigurationStoreFactory(),
repositoryLocationResolver,
null,
null
);
handler.init(context);
return handler;
}
/**
* Method description
*
*
* @return
*/
public static HgHookManager createHookManager()
{
HgHookManager hookManager = mock(HgHookManager.class);
when(hookManager.getChallenge()).thenReturn("challenge");
when(hookManager.createUrl()).thenReturn(
"http://localhost:8081/scm/hook/hg/");
when(hookManager.createUrl(any(HttpServletRequest.class))).thenReturn(
"http://localhost:8081/scm/hook/hg/");
AccessToken accessToken = mock(AccessToken.class);
when(accessToken.compact()).thenReturn("");
when(hookManager.getAccessToken()).thenReturn(accessToken);
return hookManager;
public static HgRepositoryFactory createFactory(HgRepositoryHandler handler, File directory) {
return new HgRepositoryFactory(
handler, new HookEnvironment(), new EmptyHgEnvironmentBuilder(), repository -> directory
);
}
}

View File

@@ -0,0 +1,318 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.hooks;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.ExceptionWithContext;
import sonia.scm.NotFoundException;
import sonia.scm.TransactionId;
import sonia.scm.repository.RepositoryHookType;
import sonia.scm.repository.api.HgHookMessage;
import sonia.scm.repository.api.HgHookMessageProvider;
import sonia.scm.repository.spi.HgHookContextProvider;
import sonia.scm.repository.spi.HookEventFacade;
import sonia.scm.security.CipherUtil;
import javax.annotation.Nonnull;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class DefaultHookHandlerTest {
@Mock
private HookContextProviderFactory hookContextProviderFactory;
@Mock
private HgHookContextProvider contextProvider;
@Mock
private HookEventFacade hookEventFacade;
@Mock
private HookEventFacade.HookEventHandler hookEventHandler;
@Mock
private Socket socket;
private HookEnvironment hookEnvironment;
private DefaultHookHandler handler;
@Mock
private Subject subject;
@BeforeEach
void setUp() {
ThreadContext.bind(subject);
hookEnvironment = new HookEnvironment();
handler = new DefaultHookHandler(hookContextProviderFactory, hookEventFacade, hookEnvironment, socket);
}
private void mockMessageProvider() {
mockMessageProvider(new HgHookMessageProvider());
}
private void mockMessageProvider(HgHookMessageProvider messageProvider) {
when(hookContextProviderFactory.create("42", "abc")).thenReturn(contextProvider);
when(contextProvider.getHgMessageProvider()).thenReturn(messageProvider);
}
@AfterEach
void tearDown() {
ThreadContext.unbindSubject();
}
@Test
void shouldFireHook() throws IOException {
mockMessageProvider();
when(hookEventFacade.handle("42")).thenReturn(hookEventHandler);
DefaultHookHandler.Request request = createRequest(RepositoryHookType.POST_RECEIVE);
DefaultHookHandler.Response response = send(request);
assertSuccess(response, RepositoryHookType.POST_RECEIVE);
assertThat(hookEnvironment.isPending()).isFalse();
}
@Test
void shouldSetPendingStateOnPreReceiveHooks() throws IOException {
mockMessageProvider();
when(hookEventFacade.handle("42")).thenReturn(hookEventHandler);
// we have to capture the pending state, when the hook is fired
// because the state is cleared before the method ends
AtomicReference<Boolean> ref = new AtomicReference<>(Boolean.FALSE);
doAnswer(ic -> {
ref.set(hookEnvironment.isPending());
return null;
}).when(hookEventHandler).fireHookEvent(RepositoryHookType.PRE_RECEIVE, contextProvider);
DefaultHookHandler.Request request = createRequest(RepositoryHookType.PRE_RECEIVE);
DefaultHookHandler.Response response = send(request);
assertSuccess(response, RepositoryHookType.PRE_RECEIVE);
assertThat(ref.get()).isTrue();
}
@Test
void shouldHandleUnknownFailure() throws IOException {
mockMessageProvider();
doThrow(new IllegalStateException("Something went wrong"))
.when(hookEventFacade)
.handle("42");
DefaultHookHandler.Request request = createRequest(RepositoryHookType.POST_RECEIVE);
DefaultHookHandler.Response response = send(request);
assertError(response, "unknown error");
}
@Test
void shouldHandleExceptionWithContext() throws IOException {
mockMessageProvider();
doThrow(new TestingException("Exception with Context"))
.when(hookEventFacade)
.handle("42");
DefaultHookHandler.Request request = createRequest(RepositoryHookType.POST_RECEIVE);
DefaultHookHandler.Response response = send(request);
assertError(response, "Exception with Context");
}
@Test
void shouldSendMessagesOnUnknownException() throws IOException {
mockMessageProviderWithMessages();
doThrow(new IllegalStateException("Abort it"))
.when(hookEventFacade)
.handle("42");
DefaultHookHandler.Request request = createRequest(RepositoryHookType.POST_RECEIVE);
DefaultHookHandler.Response response = send(request);
assertMessages(response, "unknown error");
}
@Test
void shouldSendMessagesOnExceptionWithContext() throws IOException {
mockMessageProviderWithMessages();
doThrow(new TestingException("Exception with Context"))
.when(hookEventFacade)
.handle("42");
DefaultHookHandler.Request request = createRequest(RepositoryHookType.POST_RECEIVE);
DefaultHookHandler.Response response = send(request);
assertMessages(response, "Exception with Context");
}
private void assertMessages(DefaultHookHandler.Response response, String errorMessage) {
List<String> received = response.getMessages()
.stream()
.map(HgHookMessage::getMessage)
.collect(Collectors.toList());
assertThat(received).containsExactly("Some note", "Some error", errorMessage);
}
private void mockMessageProviderWithMessages() {
HgHookMessageProvider messageProvider = new HgHookMessageProvider();
messageProvider.sendMessage("Some note");
messageProvider.sendMessage("Some error");
mockMessageProvider(messageProvider);
}
@Test
void shouldSetAndClearTransactionId() throws IOException {
mockMessageProvider();
AtomicReference<String> ref = new AtomicReference<>();
doAnswer(ic -> {
TransactionId.get().ifPresent(ref::set);
return null;
}).when(hookEventFacade).handle("42");
DefaultHookHandler.Request request = createRequest(RepositoryHookType.POST_RECEIVE);
send(request);
assertThat(ref).hasValue("ti21");
assertThat(TransactionId.get()).isEmpty();
}
@Test
void shouldHandleAuthenticationFailure() throws IOException {
doThrow(AuthenticationException.class)
.when(subject)
.login(any(AuthenticationToken.class));
DefaultHookHandler.Request request = createRequest(RepositoryHookType.POST_RECEIVE);
DefaultHookHandler.Response response = send(request);
assertError(response, "authentication");
}
@Test
void shouldHandleNotFoundException() throws IOException {
doThrow(NotFoundException.class)
.when(hookEventFacade)
.handle("42");
DefaultHookHandler.Request request = createRequest(RepositoryHookType.POST_RECEIVE);
DefaultHookHandler.Response response = send(request);
assertError(response, "not found");
}
@Test
void shouldReturnErrorWithInvalidChallenge() throws IOException {
DefaultHookHandler.Request request = createRequest(RepositoryHookType.POST_RECEIVE, "something-different");
DefaultHookHandler.Response response = send(request);
assertError(response, "challenge");
}
private void assertSuccess(DefaultHookHandler.Response response, RepositoryHookType type) {
assertThat(response.getMessages()).isEmpty();
assertThat(response.isAbort()).isFalse();
verify(hookEventHandler).fireHookEvent(eq(type), any(HgHookContextProvider.class));
}
private void assertError(DefaultHookHandler.Response response, String message) {
assertThat(response.isAbort()).isTrue();
assertThat(response.getMessages()).hasSize(1);
HgHookMessage hgHookMessage = response.getMessages().get(0);
assertThat(hgHookMessage.getSeverity()).isEqualTo(HgHookMessage.Severity.ERROR);
assertThat(hgHookMessage.getMessage()).contains(message);
}
@Nonnull
private DefaultHookHandler.Request createRequest(RepositoryHookType type) {
return createRequest(type, hookEnvironment.getChallenge());
}
@Nonnull
private DefaultHookHandler.Request createRequest(RepositoryHookType type, String challenge) {
String secret = CipherUtil.getInstance().encode("secret");
return new DefaultHookHandler.Request(
secret, type, "ti21", "42", challenge, "abc"
);
}
private DefaultHookHandler.Response send(DefaultHookHandler.Request request) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
Sockets.send(buffer, request);
ByteArrayInputStream input = new ByteArrayInputStream(buffer.toByteArray());
when(socket.getInputStream()).thenReturn(input);
ByteArrayOutputStream output = new ByteArrayOutputStream();
when(socket.getOutputStream()).thenReturn(output);
handler.run();
return Sockets.receive(new ByteArrayInputStream(output.toByteArray()), DefaultHookHandler.Response.class);
}
private static class TestingException extends ExceptionWithContext {
private TestingException(String message) {
super(Collections.emptyList(), message);
}
@Override
public String getCode() {
return "42";
}
}
}

View File

@@ -21,58 +21,50 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
package sonia.scm.repository.hooks;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.security.AccessToken;
import sonia.scm.security.Xsrf;
import sonia.scm.NotFoundException;
import sonia.scm.repository.HgRepositoryFactory;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryTestData;
import sonia.scm.repository.spi.HgHookContextProvider;
import java.util.HashMap;
import java.util.Map;
import static java.util.Optional.empty;
import static java.util.Optional.of;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
import static org.mockito.Mockito.mock;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class HgEnvironmentTest {
class HookContextProviderFactoryTest {
@Mock
HgRepositoryHandler handler;
private RepositoryManager repositoryManager;
@Mock
HgHookManager hookManager;
private HgRepositoryHandler repositoryHandler;
@Mock
private HgRepositoryFactory repositoryFactory;
@InjectMocks
private HookContextProviderFactory factory;
@Test
void shouldExtractXsrfTokenWhenSet() {
AccessToken accessToken = mock(AccessToken.class);
when(accessToken.compact()).thenReturn("");
when(accessToken.getCustom(Xsrf.TOKEN_KEY)).thenReturn(of("XSRF Token"));
when(hookManager.getAccessToken()).thenReturn(accessToken);
Map<String, String> environment = new HashMap<>();
HgEnvironment.prepareEnvironment(environment, handler, hookManager);
assertThat(environment).contains(entry("SCM_XSRF", "XSRF Token"));
void shouldCreateHookContextProvider() {
when(repositoryManager.get("42")).thenReturn(RepositoryTestData.create42Puzzle());
HgHookContextProvider provider = factory.create("42", "xyz");
assertThat(provider).isNotNull();
}
@Test
void shouldIgnoreXsrfWhenNotSetButStillContainDummy() {
AccessToken accessToken = mock(AccessToken.class);
when(accessToken.compact()).thenReturn("");
when(accessToken.getCustom(Xsrf.TOKEN_KEY)).thenReturn(empty());
when(hookManager.getAccessToken()).thenReturn(accessToken);
Map<String, String> environment = new HashMap<>();
HgEnvironment.prepareEnvironment(environment, handler, hookManager);
assertThat(environment).containsKeys("SCM_XSRF");
void shouldThrowNotFoundExceptionWithoutRepository() {
assertThrows(NotFoundException.class, () -> factory.create("42", "xyz"));
}
}

View File

@@ -0,0 +1,135 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.hooks;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(MockitoExtension.class)
class HookServerTest {
private static final Logger LOG = LoggerFactory.getLogger(HookServerTest.class);
@BeforeEach
void setUp() {
DefaultSecurityManager securityManager = new DefaultSecurityManager();
ThreadContext.bind(securityManager);
Subject subject = new Subject.Builder().principals(new SimplePrincipalCollection("Tricia", "Testing")).buildSubject();
ThreadContext.bind(subject);
}
@AfterEach
void tearDown() {
ThreadContext.unbindSubject();
ThreadContext.unbindSecurityManager();
}
@Test
void shouldStartHookServer() throws IOException {
Response response = send(new Request("Joe"));
assertThat(response.getGreeting()).isEqualTo("Hello Joe");
assertThat(response.getGreeter()).isEqualTo("Tricia");
}
private Response send(Request request) throws IOException {
try (HookServer server = new HookServer(HelloHandler::new)) {
int port = server.start();
try (
Socket socket = new Socket(InetAddress.getLoopbackAddress(), port);
InputStream input = socket.getInputStream();
OutputStream output = socket.getOutputStream()
) {
Sockets.send(output, request);
return Sockets.receive(input, Response.class);
} catch (IOException ex) {
throw new RuntimeException("failed", ex);
}
}
}
public static class HelloHandler implements HookHandler {
private final Socket socket;
private HelloHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try (InputStream input = socket.getInputStream(); OutputStream output = socket.getOutputStream()) {
Request request = Sockets.receive(input, Request.class);
Subject subject = SecurityUtils.getSubject();
Sockets.send(output, new Response("Hello " + request.getName(), subject.getPrincipal().toString()));
} catch (IOException ex) {
throw new RuntimeException("failed", ex);
} finally {
try {
socket.close();
} catch (IOException e) {
LOG.error("failed to close socket", e);
}
}
}
}
@Data
@AllArgsConstructor
public static class Request {
private String name;
}
@Data
@AllArgsConstructor
public static class Response {
private String greeting;
private String greeter;
}
}

View File

@@ -0,0 +1,112 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.hooks;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Stream.generate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
class SocketsTest {
@Test
void shouldSendAndReceive() throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
Sockets.send(output, new TestValue("awesome"));
ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray());
TestValue value = Sockets.receive(input, TestValue.class);
assertThat(value.value).isEqualTo("awesome");
}
@Test
void shouldFailWithTooFewBytesForLength() {
ByteArrayOutputStream output = new ByteArrayOutputStream();
output.write((512 >>> 24) & 0xFF);
output.write((512 >>> 16) & 0xFF);
ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray());
assertThrows(EOFException.class, () -> Sockets.receive(input, TestValue.class));
}
@Test
void shouldFailWithTooFewBytesForData() {
ByteArrayOutputStream output = new ByteArrayOutputStream();
output.write((16 >>> 24) & 0xFF);
output.write((16 >>> 16) & 0xFF);
output.write((16 >>> 8) & 0xFF);
output.write(16 & 0xFF);
ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray());
assertThrows(EOFException.class, () -> Sockets.receive(input, TestValue.class));
}
@Test
void shouldFailIfLimitIsExceeded() {
ByteArrayOutputStream output = new ByteArrayOutputStream();
output.write((9216 >>> 24) & 0xFF);
output.write((9216 >>> 16) & 0xFF);
output.write((9216 >>> 8) & 0xFF);
output.write(9216 & 0xFF);
ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray());
IOException ex = assertThrows(IOException.class, () -> Sockets.receive(input, TestValue.class));
assertThat(ex.getMessage()).contains("9216");
}
@Test
void shouldSendAndReceiveWithChunks() throws IOException {
String stringValue = generate(() -> "a").limit(1024).collect(joining());
ByteArrayOutputStream output = new ByteArrayOutputStream();
Sockets.send(output, new TestValue(stringValue));
InputStream input = new ByteArrayInputStream(output.toByteArray()) {
@Override
public synchronized int read(byte[] b, int off, int len) {
return super.read(b, off, Math.min(8, len));
}
};
TestValue value = Sockets.receive(input, TestValue.class);
assertThat(value.value).hasSize(1024);
}
@Data
@AllArgsConstructor
public static class TestValue {
private String value;
}
}

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
@@ -29,13 +29,16 @@ package sonia.scm.repository.spi;
import org.junit.After;
import org.junit.Before;
import sonia.scm.repository.HgRepositoryFactory;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.HgTestUtil;
import sonia.scm.repository.RepositoryTestData;
import sonia.scm.repository.hooks.HookEnvironment;
import sonia.scm.util.MockUtil;
//~--- JDK imports ------------------------------------------------------------
import java.io.File;
import java.io.IOException;
/**
@@ -49,31 +52,22 @@ public class AbstractHgCommandTestBase extends ZippedRepositoryTestBase
* Method description
*
*
* @throws IOException
*/
@After
public void close() throws IOException
{
public void close() {
if (cmdContext != null)
{
cmdContext.close();
}
}
/**
* Method description
*
*
* @throws IOException
*/
@Before
public void initHgHandler() throws IOException {
this.handler = HgTestUtil.createHandler(tempFolder.newFolder());
HgTestUtil.checkForSkip(handler);
cmdContext = new HgCommandContext(HgTestUtil.createHookManager(), handler,
RepositoryTestData.createHeartOfGold(), repositoryDirectory);
HgRepositoryFactory factory = HgTestUtil.createFactory(handler, repositoryDirectory);
cmdContext = new HgCommandContext(handler, factory, RepositoryTestData.createHeartOfGold());
}
//~--- set methods ----------------------------------------------------------

View File

@@ -29,12 +29,10 @@ import com.google.inject.util.Providers;
import org.junit.Before;
import org.junit.Test;
import sonia.scm.repository.Branch;
import sonia.scm.repository.HgTestUtil;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.api.BranchRequest;
import sonia.scm.repository.work.NoneCachingWorkingCopyPool;
import sonia.scm.repository.work.WorkdirProvider;
import sonia.scm.web.HgRepositoryEnvironmentBuilder;
import java.util.List;
@@ -47,10 +45,8 @@ public class HgBranchCommandTest extends AbstractHgCommandTestBase {
@Before
public void initWorkingCopyFactory() {
HgRepositoryEnvironmentBuilder hgRepositoryEnvironmentBuilder =
new HgRepositoryEnvironmentBuilder(handler, HgTestUtil.createHookManager());
workingCopyFactory = new SimpleHgWorkingCopyFactory(Providers.of(hgRepositoryEnvironmentBuilder), new NoneCachingWorkingCopyPool(new WorkdirProvider())) {
workingCopyFactory = new SimpleHgWorkingCopyFactory(new NoneCachingWorkingCopyPool(new WorkdirProvider())) {
@Override
public void configure(PullCommand pullCommand) {
// we do not want to configure http hooks in this unit test

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
@@ -110,17 +110,10 @@ public class HgIncomingCommandTest extends IncomingOutgoingTestBase
cmd.getIncomingChangesets(request);
}
/**
* Method description
*
*
* @return
*/
private HgIncomingCommand createIncomingCommand()
{
private HgIncomingCommand createIncomingCommand() {
return new HgIncomingCommand(
new HgCommandContext(
HgTestUtil.createHookManager(), handler, incomingRepository,
incomingDirectory), handler);
new HgCommandContext(handler, HgTestUtil.createFactory(handler, incomingDirectory), incomingRepository),
handler
);
}
}

View File

@@ -40,12 +40,11 @@ import static org.assertj.core.api.Assertions.assertThat;
public class HgModificationsCommandTest extends IncomingOutgoingTestBase {
private HgModificationsCommand outgoingModificationsCommand;
@Before
public void init() {
HgCommandContext outgoingContext = new HgCommandContext(HgTestUtil.createHookManager(), handler, outgoingRepository, outgoingDirectory);
HgCommandContext outgoingContext = new HgCommandContext(handler, HgTestUtil.createFactory(handler, outgoingDirectory), outgoingRepository);
outgoingModificationsCommand = new HgModificationsCommand(outgoingContext);
}
@@ -116,10 +115,10 @@ public class HgModificationsCommandTest extends IncomingOutgoingTestBase {
assertThat(modifications).isNotNull();
assertThat(modifications.getAdded())
.as("added files modifications")
.hasSize(0);
.isEmpty();
assertThat(modifications.getModified())
.as("modified files modifications")
.hasSize(0);
.isEmpty();
assertThat(modifications.getRemoved())
.as("removed files modifications")
.hasSize(1)
@@ -136,10 +135,10 @@ public class HgModificationsCommandTest extends IncomingOutgoingTestBase {
assertThat(modifications).isNotNull();
assertThat(modifications.getAdded())
.as("added files modifications")
.hasSize(0);
.isEmpty();
assertThat(modifications.getModified())
.as("modified files modifications")
.hasSize(0);
.isEmpty();
assertThat(modifications.getRemoved())
.as("removed files modifications")
.isEmpty();
@@ -161,10 +160,10 @@ public class HgModificationsCommandTest extends IncomingOutgoingTestBase {
assertThat(modifications).isNotNull();
assertThat(modifications.getAdded())
.as("added files modifications")
.hasSize(0);
.isEmpty();
assertThat(modifications.getModified())
.as("modified files modifications")
.hasSize(0);
.isEmpty();
assertThat(modifications.getRemoved())
.as("removed files modifications")
.isEmpty();
@@ -189,7 +188,7 @@ public class HgModificationsCommandTest extends IncomingOutgoingTestBase {
assertThat(modifications).isNotNull();
assertThat(modifications.getAdded())
.as("added files modifications")
.hasSize(0);
.isEmpty();
assertThat(modifications.getModified())
.as("modified files modifications")
.hasSize(1)
@@ -197,10 +196,10 @@ public class HgModificationsCommandTest extends IncomingOutgoingTestBase {
.containsOnly(file);
assertThat(modifications.getRemoved())
.as("removed files modifications")
.hasSize(0);
.isEmpty();
assertThat(modifications.getRenamed())
.as("renamed files modifications")
.hasSize(0);
.isEmpty();
};
}
@@ -214,13 +213,13 @@ public class HgModificationsCommandTest extends IncomingOutgoingTestBase {
.containsOnly(addedFile);
assertThat(modifications.getModified())
.as("modified files modifications")
.hasSize(0);
.isEmpty();
assertThat(modifications.getRemoved())
.as("removed files modifications")
.hasSize(0);
.isEmpty();
assertThat(modifications.getRenamed())
.as("renamed files modifications")
.hasSize(0);
.isEmpty();
};
}
}

View File

@@ -24,7 +24,6 @@
package sonia.scm.repository.spi;
import com.google.inject.util.Providers;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -32,12 +31,9 @@ import org.junit.rules.TemporaryFolder;
import sonia.scm.AlreadyExistsException;
import sonia.scm.NoChangesMadeException;
import sonia.scm.NotFoundException;
import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.HgTestUtil;
import sonia.scm.repository.Person;
import sonia.scm.repository.work.NoneCachingWorkingCopyPool;
import sonia.scm.repository.work.WorkdirProvider;
import sonia.scm.web.HgRepositoryEnvironmentBuilder;
import java.io.File;
import java.io.FileOutputStream;
@@ -55,9 +51,7 @@ public class HgModifyCommandTest extends AbstractHgCommandTestBase {
@Before
public void initHgModifyCommand() {
HgHookManager hookManager = HgTestUtil.createHookManager();
HgRepositoryEnvironmentBuilder environmentBuilder = new HgRepositoryEnvironmentBuilder(handler, hookManager);
SimpleHgWorkingCopyFactory workingCopyFactory = new SimpleHgWorkingCopyFactory(Providers.of(environmentBuilder), new NoneCachingWorkingCopyPool(new WorkdirProvider())) {
SimpleHgWorkingCopyFactory workingCopyFactory = new SimpleHgWorkingCopyFactory(new NoneCachingWorkingCopyPool(new WorkdirProvider())) {
@Override
public void configure(com.aragost.javahg.commands.PullCommand pullCommand) {
// we do not want to configure http hooks in this unit test

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
@@ -106,17 +106,10 @@ public class HgOutgoingCommandTest extends IncomingOutgoingTestBase
System.out.println(cpr.getChangesets());
}
/**
* Method description
*
*
* @return
*/
private HgOutgoingCommand createOutgoingCommand()
{
private HgOutgoingCommand createOutgoingCommand() {
return new HgOutgoingCommand(
new HgCommandContext(
HgTestUtil.createHookManager(), handler, outgoingRepository,
outgoingDirectory), handler);
new HgCommandContext(handler, HgTestUtil.createFactory(handler, outgoingDirectory), outgoingRepository),
handler
);
}
}

View File

@@ -0,0 +1,137 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.spi;
import com.google.common.base.Joiner;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgVersion;
import javax.annotation.Nonnull;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class HgVersionCommandTest {
private static final String PYTHON_OUTPUT = String.join("\n",
"3.9.0 (default, Oct 27 2020, 14:15:17)",
"[Clang 12.0.0 (clang-1200.0.32.21)]"
);
private Map<String, Process> outputs;
@BeforeEach
void setUpOutputs() {
outputs = new HashMap<>();
}
@Test
void shouldReturnHgVersion() throws InterruptedException {
command("/usr/local/bin/hg", HgVersionCommand.HG_ARGS, "5.5.2", 0);
command("/opt/python/bin/python", HgVersionCommand.PYTHON_ARGS, PYTHON_OUTPUT, 0);
HgVersion hgVersion = getVersion("/usr/local/bin/hg", "/opt/python/bin/python");
assertThat(hgVersion.getMercurial()).isEqualTo("5.5.2");
assertThat(hgVersion.getPython()).isEqualTo("3.9.0");
}
@Test
void shouldReturnUnknownMercurialVersionOnNonZeroExitCode() throws InterruptedException {
command("hg", HgVersionCommand.HG_ARGS, "", 1);
command("python", HgVersionCommand.PYTHON_ARGS, PYTHON_OUTPUT, 0);
HgVersion hgVersion = getVersion("hg", "python");
assertThat(hgVersion.getMercurial()).isEqualTo(HgVersion.UNKNOWN);
assertThat(hgVersion.getPython()).isEqualTo("3.9.0");
}
@Test
void shouldReturnUnknownPythonVersionOnNonZeroExitCode() throws InterruptedException {
command("hg", HgVersionCommand.HG_ARGS, "4.4.2", 0);
command("python", HgVersionCommand.PYTHON_ARGS, "", 1);
HgVersion hgVersion = getVersion("hg", "python");
assertThat(hgVersion.getMercurial()).isEqualTo("4.4.2");
assertThat(hgVersion.getPython()).isEqualTo(HgVersion.UNKNOWN);
}
@Test
void shouldReturnUnknownForInvalidPythonOutput() throws InterruptedException {
command("hg", HgVersionCommand.HG_ARGS, "1.0.0", 0);
command("python", HgVersionCommand.PYTHON_ARGS, "abcdef", 0);
HgVersion hgVersion = getVersion("hg", "python");
assertThat(hgVersion.getMercurial()).isEqualTo("1.0.0");
assertThat(hgVersion.getPython()).isEqualTo(HgVersion.UNKNOWN);
}
@Test
void shouldReturnUnknownForIOException() {
HgVersionCommand command = new HgVersionCommand(new HgConfig(), cmd -> {
throw new IOException("failed");
});
HgVersion hgVersion = command.get();
assertThat(hgVersion.getMercurial()).isEqualTo(HgVersion.UNKNOWN);
assertThat(hgVersion.getPython()).isEqualTo(HgVersion.UNKNOWN);
}
private Process command(String command, String[] args, String content, int exitValue) throws InterruptedException {
Process process = mock(Process.class);
when(process.getInputStream()).thenReturn(new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)));
when(process.waitFor()).thenReturn(exitValue);
List<String> cmdLine = new ArrayList<>();
cmdLine.add(command);
cmdLine.addAll(Arrays.asList(args));
outputs.put(Joiner.on(' ').join(cmdLine), process);
return process;
}
@Nonnull
private HgVersion getVersion(String hg, String python) {
HgConfig config = new HgConfig();
config.setHgBinary(hg);
config.setPythonBinary(python);
return new HgVersionCommand(config, command -> outputs.get(Joiner.on(' ').join(command))).get();
}
}

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
@@ -39,7 +39,6 @@ import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
import sonia.scm.AbstractTestBase;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgContext;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.HgTestUtil;
import sonia.scm.user.User;
@@ -88,7 +87,6 @@ public abstract class IncomingOutgoingTestBase extends AbstractTestBase
when(handler.getDirectory(outgoingRepository.getId())).thenReturn(
outgoingDirectory);
when(handler.getConfig()).thenReturn(temp.getConfig());
when(handler.getHgContext()).thenReturn(new HgContext());
}
//~--- set methods ----------------------------------------------------------

View File

@@ -30,12 +30,11 @@ import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.HgEnvironmentBuilder;
import sonia.scm.repository.HgTestUtil;
import sonia.scm.repository.work.SimpleCachingWorkingCopyPool;
import sonia.scm.repository.work.WorkdirProvider;
import sonia.scm.repository.work.WorkingCopy;
import sonia.scm.web.HgRepositoryEnvironmentBuilder;
import java.io.File;
import java.io.IOException;
@@ -57,9 +56,7 @@ public class SimpleHgWorkingCopyFactoryTest extends AbstractHgCommandTestBase {
@Before
public void bindScmProtocol() throws IOException {
workdirProvider = new WorkdirProvider(temporaryFolder.newFolder());
HgHookManager hookManager = HgTestUtil.createHookManager();
HgRepositoryEnvironmentBuilder environmentBuilder = new HgRepositoryEnvironmentBuilder(handler, hookManager);
workingCopyFactory = new SimpleHgWorkingCopyFactory(Providers.of(environmentBuilder), new SimpleCachingWorkingCopyPool(workdirProvider)) {
workingCopyFactory = new SimpleHgWorkingCopyFactory(new SimpleCachingWorkingCopyPool(workdirProvider)) {
@Override
public void configure(com.aragost.javahg.commands.PullCommand pullCommand) {
// we do not want to configure http hooks in this unit test

View File

@@ -1,60 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.web;
import org.junit.Test;
import sonia.scm.repository.HgRepositoryHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static sonia.scm.web.HgHookCallbackServlet.PARAM_REPOSITORYID;
public class HgHookCallbackServletTest {
@Test
public void shouldExtractCorrectRepositoryId() throws ServletException, IOException {
HgRepositoryHandler handler = mock(HgRepositoryHandler.class);
HgHookCallbackServlet servlet = new HgHookCallbackServlet(null, handler, null, null);
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
when(request.getContextPath()).thenReturn("http://example.com/scm");
when(request.getRequestURI()).thenReturn("http://example.com/scm/hook/hg/pretxnchangegroup");
String path = "/tmp/hg/12345";
when(request.getParameter(PARAM_REPOSITORYID)).thenReturn(path);
servlet.doPost(request, response);
verify(response, never()).sendError(anyInt());
}
}

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.filter;
//~--- non-JDK imports --------------------------------------------------------
@@ -34,6 +34,7 @@ import org.apache.shiro.subject.Subject;
import org.slf4j.MDC;
import sonia.scm.SCMContext;
import sonia.scm.TransactionId;
import sonia.scm.security.DefaultKeyGenerator;
import sonia.scm.web.filter.HttpFilter;
@@ -72,9 +73,6 @@ public class MDCFilter extends HttpFilter
@VisibleForTesting
static final String MDC_USERNAME = "username";
@VisibleForTesting
static final String MDC_TRANSACTION_ID = "transaction_id";
//~--- methods --------------------------------------------------------------
/**
@@ -98,7 +96,7 @@ public class MDCFilter extends HttpFilter
MDC.put(MDC_CLIENT_HOST, request.getRemoteHost());
MDC.put(MDC_REQUEST_METHOD, request.getMethod());
MDC.put(MDC_REQUEST_URI, request.getRequestURI());
MDC.put(MDC_TRANSACTION_ID, TRANSACTION_KEY_GENERATOR.createKey());
TransactionId.set(TRANSACTION_KEY_GENERATOR.createKey());
try
{
@@ -111,7 +109,7 @@ public class MDCFilter extends HttpFilter
MDC.remove(MDC_CLIENT_HOST);
MDC.remove(MDC_REQUEST_METHOD);
MDC.remove(MDC_REQUEST_URI);
MDC.remove(MDC_TRANSACTION_ID);
TransactionId.clear();
}
}

View File

@@ -36,6 +36,9 @@ import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.authz.permission.PermissionResolver;
import org.apache.shiro.crypto.hash.DefaultHashService;
import org.apache.shiro.guice.web.ShiroWebModule;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SubjectDAO;
import org.apache.shiro.realm.Realm;
import org.slf4j.Logger;
@@ -114,14 +117,24 @@ public class ScmSecurityModule extends ShiroWebModule
}
// bind constant
bindConstant().annotatedWith(Names.named("shiro.loginUrl")).to(
"/index.html");
bindConstant().annotatedWith(Names.named("shiro.loginUrl")).to("/index.html");
// disable access to mustache resources
addFilterChain("/**.mustache", filterConfig(ROLES, "nobody"));
// disable session
disableSession();
}
private void disableSession() {
addFilterChain("/**", NO_SESSION_CREATION);
bindConstant().annotatedWith(Names.named("shiro.sessionStorageEnabled")).to(false);
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultSessionStorageEvaluator();
sessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator);
bind(SubjectDAO.class).toInstance(subjectDAO);
}
/**

View File

@@ -24,12 +24,14 @@
package sonia.scm.security;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.Maps;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.subject.Subject;
@@ -56,6 +58,12 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
*/
private static final Logger LOG = LoggerFactory.getLogger(JwtAccessTokenBuilder.class);
@VisibleForTesting
static final long DEFAULT_REFRESHABLE = 12L;
@VisibleForTesting
static final TimeUnit DEFAULT_REFRESHABLE_UNIT = TimeUnit.HOURS;
private final KeyGenerator keyGenerator;
private final SecureKeyResolver keyResolver;
private final Clock clock;
@@ -64,8 +72,8 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
private String issuer;
private long expiresIn = 1;
private TimeUnit expiresInUnit = TimeUnit.HOURS;
private long refreshableFor = 12;
private TimeUnit refreshableForUnit = TimeUnit.HOURS;
private long refreshableFor = DEFAULT_REFRESHABLE;
private TimeUnit refreshableForUnit = DEFAULT_REFRESHABLE_UNIT;
private Instant refreshExpiration;
private String parentKeyId;
private Scope scope = Scope.empty();
@@ -87,8 +95,6 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
@Override
public JwtAccessTokenBuilder custom(String key, Object value) {
Preconditions.checkArgument(!Strings.isNullOrEmpty(key), "null or empty value not allowed");
Preconditions.checkArgument(value != null, "null or empty value not allowed");
this.custom.put(key, value);
return this;
}
@@ -183,8 +189,8 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
if (refreshableFor > 0) {
long refreshExpiration = refreshableForUnit.toMillis(refreshableFor);
claims.put(JwtAccessToken.REFRESHABLE_UNTIL_CLAIM_KEY, new Date(now.toEpochMilli() + refreshExpiration).getTime());
long re = refreshableForUnit.toMillis(refreshableFor);
claims.put(JwtAccessToken.REFRESHABLE_UNTIL_CLAIM_KEY, Date.from(now.plusMillis(re)));
} else if (refreshExpiration != null) {
claims.put(JwtAccessToken.REFRESHABLE_UNTIL_CLAIM_KEY, Date.from(refreshExpiration));
}
@@ -198,10 +204,11 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
claims.setIssuer(issuer);
}
// sign token and create compact version
String compact = Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS256, key.getBytes())
.signWith(Keys.hmacShaKeyFor(key.getBytes()), SignatureAlgorithm.HS256)
.compact();
return new JwtAccessToken(claims, compact);

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.filter;
import com.github.sdorra.shiro.ShiroRule;
@@ -34,6 +34,7 @@ import org.mockito.junit.MockitoJUnitRunner;
import org.slf4j.MDC;
import sonia.scm.AbstractTestBase;
import sonia.scm.SCMContext;
import sonia.scm.TransactionId;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
@@ -50,28 +51,28 @@ import static org.mockito.Mockito.when;
/**
* Unit tests for {@link MDCFilter}.
*
*
* @author Sebastian Sdorra <sebastian.sdorra@gmail.com>
*/
@RunWith(MockitoJUnitRunner.Silent.class)
public class MDCFilterTest extends AbstractTestBase {
@Rule
public ShiroRule shiro = new ShiroRule();
@Mock
private HttpServletRequest request;
@Mock
private HttpServletResponse response;
private final MDCFilter filter = new MDCFilter();
/**
* Tests {@link MDCFilter#doFilter(HttpServletRequest, HttpServletResponse, FilterChain)}.
*
*
* @throws IOException
* @throws ServletException
* @throws ServletException
*/
@Test
@SubjectAware(
@@ -85,44 +86,44 @@ public class MDCFilterTest extends AbstractTestBase {
when(request.getRemoteAddr()).thenReturn("127.0.0.1");
when(request.getRemoteHost()).thenReturn("localhost");
when(request.getMethod()).thenReturn("GET");
MDCCapturingFilterChain chain = new MDCCapturingFilterChain();
filter.doFilter(request, response, chain);
assertNotNull(chain.ctx);
assertEquals("trillian", chain.ctx.get(MDCFilter.MDC_USERNAME));
assertEquals("api/v1/repositories", chain.ctx.get(MDCFilter.MDC_REQUEST_URI));
assertEquals("127.0.0.1", chain.ctx.get(MDCFilter.MDC_CLIENT_IP));
assertEquals("localhost", chain.ctx.get(MDCFilter.MDC_CLIENT_HOST));
assertEquals("GET", chain.ctx.get(MDCFilter.MDC_REQUEST_METHOD));
assertNotNull(chain.ctx.get(MDCFilter.MDC_TRANSACTION_ID));
assertNotNull(chain.ctx.get(TransactionId.KEY));
}
/**
* Tests {@link MDCFilter#doFilter(HttpServletRequest, HttpServletResponse, FilterChain)} as anonymous user.
*
*
* @throws IOException
* @throws ServletException
* @throws ServletException
*/
@Test
@SubjectAware
public void testDoFilterAsAnonymous() throws IOException, ServletException {
MDCCapturingFilterChain chain = new MDCCapturingFilterChain();
filter.doFilter(request, response, chain);
assertNotNull(chain.ctx);
assertEquals(SCMContext.USER_ANONYMOUS, chain.ctx.get(MDCFilter.MDC_USERNAME));
}
private static class MDCCapturingFilterChain implements FilterChain {
private Map<String, String> ctx;
@Override
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
this.ctx = MDC.getCopyOfContextMap();
}
}
}

View File

@@ -39,15 +39,17 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.Clock;
import java.time.Instant;
import java.util.Collection;
import java.util.Date;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import static java.util.Collections.singleton;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.*;
import static sonia.scm.security.SecureKeyTestUtil.createSecureKey;
/**
@@ -85,52 +87,107 @@ class JwtAccessTokenBuilderTest {
ThreadContext.unbindSubject();
}
/**
* Prepare mocks and set up object under test.
*/
@BeforeEach
void setUpObjectUnderTest() {
void setUpDependencies() {
lenient().when(keyGenerator.createKey()).thenReturn("42");
lenient().when(secureKeyResolver.getSecureKey(anyString())).thenReturn(createSecureKey());
enrichers = Sets.newHashSet();
factory = new JwtAccessTokenBuilderFactory(keyGenerator, secureKeyResolver, enrichers);
}
/**
* Tests {@link JwtAccessTokenBuilder#build()}.
*/
@Test
void testBuild() {
JwtAccessToken token = factory.create().subject("dent")
.issuer("https://www.scm-manager.org")
.expiresIn(1, TimeUnit.MINUTES)
.custom("a", "b")
.scope(Scope.valueOf("repo:*"))
.build();
@Nested
class SimpleTests {
// assert claims
assertClaims(token);
/**
* Prepare mocks and set up object under test.
*/
@BeforeEach
void setUpObjectUnderTest() {
factory = new JwtAccessTokenBuilderFactory(keyGenerator, secureKeyResolver, enrichers);
}
/**
* Tests {@link JwtAccessTokenBuilder#build()}.
*/
@Test
void testBuild() {
JwtAccessToken token = factory.create().subject("dent")
.issuer("https://www.scm-manager.org")
.expiresIn(1, TimeUnit.MINUTES)
.custom("a", "b")
.scope(Scope.valueOf("repo:*"))
.build();
// assert claims
assertClaims(token);
// reparse and assert again
String compact = token.compact();
assertThat(compact).isNotEmpty();
Claims claims = Jwts.parser()
.setSigningKey(secureKeyResolver.getSecureKey("dent").getBytes())
.parseClaimsJws(compact)
.getBody();
assertClaims(new JwtAccessToken(claims, compact));
}
private void assertClaims(JwtAccessToken token) {
assertThat(token.getId()).isNotEmpty();
assertThat(token.getIssuedAt()).isNotNull();
assertThat(token.getExpiration()).isNotNull();
assertThat(token.getExpiration().getTime() > token.getIssuedAt().getTime()).isTrue();
assertThat(token.getSubject()).isEqualTo("dent");
assertThat(token.getIssuer()).isNotEmpty();
assertThat(token.getIssuer()).get().isEqualTo("https://www.scm-manager.org");
assertThat(token.getCustom("a")).get().isEqualTo("b");
assertThat(token.getScope()).hasToString("[\"repo:*\"]");
}
// reparse and assert again
String compact = token.compact();
assertThat(compact).isNotEmpty();
Claims claims = Jwts.parser()
.setSigningKey(secureKeyResolver.getSecureKey("dent").getBytes())
.parseClaimsJws(compact)
.getBody();
assertClaims(new JwtAccessToken(claims, compact));
}
private void assertClaims(JwtAccessToken token) {
assertThat(token.getId()).isNotEmpty();
assertThat(token.getIssuedAt()).isNotNull();
assertThat(token.getExpiration()).isNotNull();
assertThat(token.getExpiration().getTime() > token.getIssuedAt().getTime()).isTrue();
assertThat(token.getSubject()).isEqualTo("dent");
assertThat(token.getIssuer()).isNotEmpty();
assertThat(token.getIssuer()).get().isEqualTo("https://www.scm-manager.org");
assertThat(token.getCustom("a")).get().isEqualTo("b");
assertThat(token.getScope()).hasToString("[\"repo:*\"]");
@Nested
class ClockTests {
@Mock
private Clock clock;
@BeforeEach
void setUpObjectUnderTest() {
factory = new JwtAccessTokenBuilderFactory(keyGenerator, secureKeyResolver, enrichers, clock);
}
@Test
void shouldSetRefreshExpiration() {
Instant now = Instant.now();
when(clock.instant()).thenReturn(now);
JwtAccessToken token = factory.create()
.subject("dent")
.refreshableFor(2, TimeUnit.SECONDS)
.build();
assertThat(token.getRefreshExpiration()).isPresent();
Date date = token.getRefreshExpiration().get();
assertThat(date).hasSameTimeAs(Date.from(now.plusSeconds(2L)));
}
@Test
void shouldSetDefaultRefreshExpiration() {
Instant now = Instant.now();
when(clock.instant()).thenReturn(now);
JwtAccessToken token = factory.create()
.subject("dent")
.build();
assertThat(token.getRefreshExpiration()).isPresent();
Date date = token.getRefreshExpiration().get();
long defaultRefresh = JwtAccessTokenBuilder.DEFAULT_REFRESHABLE_UNIT.toMillis(JwtAccessTokenBuilder.DEFAULT_REFRESHABLE);
assertThat(date).hasSameTimeAs(Date.from(now.plusMillis(defaultRefresh)));
}
}
@Nested