Commit 932d26ec authored by zeroleak's avatar zeroleak
Browse files

add cli.api.require-https

parent a11080cf
# whirlpool-client-cli API
whirlpool-client-cli exposes a REST API when started with --listen[=8899].
whirlpool-client-cli exposes a REST API over HTTPS when started with --listen.
HTTPS port is defined in `whirlpool-cli-config.properties`:
```
cli.api.port-https=8899
```
HTTPS requirement can be disabled at your own risk:
```
cli.api.require-https=false
cli.api.port-http=8898
```
#### API KEY
API key is configured in ```whirlpool-cli-config.properties```.
......@@ -12,7 +23,7 @@ It can be overriden with ```--api-key=```
* apiKey
#### HTTPS
#### HTTPS cert
REST API uses a self-signed certificate for HTTPS.
You can configure your own cert in `whirlpool-cli-config.properties`:
```
......@@ -22,11 +33,6 @@ server.ssl.key-store-password=<passord>
server.ssl.key-alias=<alias in keystore>
```
HTTPS can be disabled at your own risk:
```
security.require-ssl=false
```
## Pools
### List pools: ```GET /rest/pools[?tx0FeeTarget=BLOCKS_24]```
......
......@@ -105,11 +105,7 @@ public class Application implements ApplicationRunner {
if (log.isDebugEnabled()) {
log.debug("[cli/debug] debug=" + debug + ", debugClient=" + debugClient);
log.debug("[cli/protocolVersion] " + WhirlpoolProtocol.PROTOCOL_VERSION);
log.debug(
"[cli/listen] "
+ (listen
? listenPort + ", https=" + System.getProperty("security.require-ssl")
: "false"));
log.debug("[cli/listen] " + (listen ? listenPort : "false"));
}
try {
......@@ -164,4 +160,8 @@ public class Application implements ApplicationRunner {
.filter(a -> !a.toLowerCase().equals("--" + ApplicationArgs.ARG_INIT))
.toArray(i -> new String[i]);
}
public static Integer getListenPort() {
return listenPort;
}
}
......@@ -31,6 +31,7 @@ public class ApiCliConfig {
private static final String KEY_MIX_TX0_MAX_OUTPUTS = "cli.mix.tx0MaxOutputs";
private static final String KEY_MIX_AUTO_MIX = "cli.mix.autoMix";
private static final String KEY_MIX_MIXS_TARGET = "cli.mix.mixsTarget";
public static final String KEY_API_REQUIRE_HTTPS = "cli.api.require-https";
public ApiCliConfig() {}
......
......@@ -37,6 +37,7 @@ public abstract class CliConfigFile {
@NotEmpty private String proxy;
private Optional<CliProxy> _cliProxy;
@NotEmpty private MixConfig mix;
@NotEmpty private ApiConfig api;
private static final String PUSHTX_AUTO = "auto";
private static final String PUSHTX_INTERACTIVE = "interactive";
......@@ -60,6 +61,7 @@ public abstract class CliConfigFile {
this.refreshPoolsDelay = copy.refreshPoolsDelay;
this.tx0MinConfirmations = copy.tx0MinConfirmations;
this.proxy = copy.proxy;
this.api = new ApiConfig(copy.api);
this.mix = new MixConfig(copy.mix);
}
......@@ -182,6 +184,14 @@ public abstract class CliConfigFile {
this.mix = mix;
}
public ApiConfig getApi() {
return api;
}
public void setApi(ApiConfig api) {
this.api = api;
}
public static class MixConfig {
@NotEmpty private Integer clients;
@NotEmpty private int clientsPerPool;
......@@ -272,6 +282,57 @@ public abstract class CliConfigFile {
}
}
public static class ApiConfig {
@NotEmpty private int portHttps;
@NotEmpty private int portHttp;
@NotEmpty private boolean requireHttps;
public ApiConfig() {}
public ApiConfig(ApiConfig copy) {
this.portHttps = copy.portHttps;
this.portHttp = copy.portHttp;
this.requireHttps = copy.requireHttps;
}
public int getPortHttps() {
return portHttps;
}
public void setPortHttps(int portHttps) {
this.portHttps = portHttps;
}
public int getPortHttp() {
return portHttp;
}
public void setPortHttp(int portHttp) {
this.portHttp = portHttp;
}
public boolean isRequireHttps() {
return requireHttps;
}
public void setRequireHttps(boolean requireHttps) {
this.requireHttps = requireHttps;
}
public Map<String, String> getConfigInfo() {
Map<String, String> configInfo = new HashMap<>();
configInfo.put(
"cli/api",
"portHttps="
+ portHttps
+ ", portHttp="
+ portHttp
+ ", requireHttps="
+ Boolean.toString(requireHttps));
return configInfo;
}
}
public static class TorConfig {
public static final String EXECUTABLE_AUTO = "auto";
public static final String EXECUTABLE_LOCAL = "local";
......@@ -426,6 +487,7 @@ public abstract class CliConfigFile {
configInfo.put("cli/tx0MinConfirmations", Integer.toString(tx0MinConfirmations));
configInfo.put("cli/proxy", proxy != null ? ClientUtils.maskString(proxy) : "null");
configInfo.putAll(mix.getConfigInfo());
configInfo.putAll(api.getConfigInfo());
return configInfo;
}
}
......@@ -2,10 +2,15 @@ package com.samourai.whirlpool.cli.config;
import com.samourai.wallet.hd.java.HD_WalletFactoryJava;
import com.samourai.wallet.segwit.bech32.Bech32UtilGeneric;
import com.samourai.whirlpool.cli.api.protocol.beans.ApiCliConfig;
import java.lang.invoke.MethodHandles;
import org.apache.catalina.connector.Connector;
import org.bitcoinj.core.NetworkParameters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
......@@ -59,4 +64,19 @@ public class CliServicesConfig {
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
@Bean
@ConditionalOnProperty(name = ApiCliConfig.KEY_API_REQUIRE_HTTPS, havingValue = "false")
public ServletWebServerFactory httpServer(CliConfig cliConfig) {
// https not required => configure HTTP server
int portHttp = cliConfig.getApi().getPortHttp();
if (log.isDebugEnabled()) {
log.debug("Enabling API over HTTP... portHttp=" + portHttp);
}
Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL);
connector.setPort(portHttp);
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
factory.addAdditionalTomcatConnectors(connector);
return factory;
}
}
package com.samourai.whirlpool.cli.config.security;
import com.samourai.whirlpool.cli.api.protocol.CliApiEndpoint;
import com.samourai.whirlpool.cli.config.CliConfig;
import java.lang.invoke.MethodHandles;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
......@@ -11,9 +15,21 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
@EnableWebSecurity
public class CliWebSecurityConfig extends WebSecurityConfigurerAdapter {
private Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private CliConfig cliConfig;
public CliWebSecurityConfig(CliConfig cliConfig) {
this.cliConfig = cliConfig;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
boolean requireHttps = cliConfig.getApi().isRequireHttps();
if (log.isDebugEnabled()) {
log.debug("Configuring REST API: requireHttps=" + requireHttps);
}
// disable CSRF
http.csrf()
.disable()
......@@ -26,5 +42,9 @@ public class CliWebSecurityConfig extends WebSecurityConfigurerAdapter {
// reject others
.anyRequest()
.denyAll();
if (requireHttps) {
http.requiresChannel().anyRequest().requiresSecure();
}
}
}
......@@ -88,7 +88,7 @@ public class CliStatusOrchestrator extends AbstractOrchestrator {
+ mixingState.getNbQueued()
+ " queued. Commands: [T]hreads, [D]eposit, [P]remix, P[O]stmix\r");
} catch (NoSessionWalletException e) {
System.out.print("⣿ Wallet CLOSED");
System.out.print("⣿ Wallet CLOSED\r");
} catch (Exception e) {
log.error("", e);
}
......@@ -112,7 +112,7 @@ public class CliStatusOrchestrator extends AbstractOrchestrator {
i++;
}
} catch (NoSessionWalletException e) {
System.out.print("⣿ Wallet CLOSED");
System.out.print("⣿ Wallet CLOSED\r");
} catch (Exception e) {
log.error("", e);
}
......
......@@ -118,6 +118,14 @@ public class CliService {
return true; // restart
}
if (listen) {
String info = "API listening on https://127.0.0.1:" + cliConfig.getApi().getPortHttps();
if (!cliConfig.getApi().isRequireHttps()) {
info += " and http://127.0.0.1:" + cliConfig.getApi().getPortHttp();
}
log.info(info);
}
if (!appArgs.isAuthenticate() && listen && !RunCliCommand.hasCommandToRun(appArgs, cliConfig)) {
// no passphrase but listening => keep listening
log.info(CliUtils.LOG_SEPARATOR);
......
......@@ -8,7 +8,9 @@ server.ssl.key-store-type=PKCS12
server.ssl.key-store=classpath:keystore/whirlpool.p12
server.ssl.key-store-password=whirlpool
server.ssl.key-alias=whirlpool
security.require-ssl=true
cli.api.port-https=8899
cli.api.port-http=8898
cli.api.require-https=true
# cli.version
cli.server = TESTNET
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment