mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-01-10 17:42:11 +01:00
Merge pull request #1416 from scm-manager/feature/hg_hooks_over_tcp
Feature/hg hooks over tcp
This commit is contained in:
@@ -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))
|
||||
|
||||
|
||||
2
pom.xml
2
pom.xml
@@ -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>
|
||||
|
||||
@@ -250,6 +250,12 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
71
scm-core/src/main/java/sonia/scm/TransactionId.java
Normal file
71
scm-core/src/main/java/sonia/scm/TransactionId.java
Normal 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);
|
||||
}
|
||||
}
|
||||
42
scm-core/src/test/java/sonia/scm/TransactionIdTest.java
Normal file
42
scm-core/src/test/java/sonia/scm/TransactionIdTest.java
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -39,7 +39,5 @@ interface UpdateHgConfigDto {
|
||||
|
||||
boolean isShowRevisionInId();
|
||||
|
||||
boolean isDisableHookSSLValidation();
|
||||
|
||||
boolean isEnableHttpPostArgs();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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("-"));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 ----------------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 ----------------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
mock-maker-inline
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user