From 9ca06f7cd15cc61b2fda2e6b5eeac6e17eaf7bab Mon Sep 17 00:00:00 2001 From: Bernard Labno Date: Wed, 9 Jan 2019 12:24:35 +0100 Subject: [PATCH] Minimal HTTP API --- .dockerignore | 25 ++++++ .travis.yml | 33 +++++-- build.gradle | 80 +++++++++++++++++ .../java/bisq/core/app/AppOptionKeys.java | 3 + .../java/bisq/core/app/BisqEnvironment.java | 14 +++ .../java/bisq/core/app/BisqExecutable.java | 9 ++ .../java/bisq/desktop/app/BisqAppMain.java | 13 +-- gradle/witness/gradle-witness.gradle | 68 ++++++++++++--- http-api/.dockerignore | 17 ++++ http-api/docker-compose-base.yml | 34 ++++++++ http-api/docker-compose.yml | 51 +++++++++++ http-api/docker/dev/Dockerfile | 29 +++++++ http-api/docker/startApi.sh | 43 ++++++++++ .../main/java/bisq/httpapi/HttpApiModule.java | 51 +++++++++++ .../bisq/httpapi/app/HttpApiHeadlessApp.java | 15 ++++ .../httpapi/app/HttpApiHeadlessModule.java | 44 ++++++++++ .../java/bisq/httpapi/app/HttpApiMain.java | 69 +++++++++++++++ .../bisq/httpapi/model/VersionDetails.java | 11 +++ .../httpapi/service/HttpApiInterfaceV1.java | 33 +++++++ .../bisq/httpapi/service/HttpApiServer.java | 86 +++++++++++++++++++ .../service/endpoint/VersionEndpoint.java | 30 +++++++ .../META-INF/custom-swagger-ui/index.html | 58 +++++++++++++ .../main/resources/openapi-configuration.json | 6 ++ .../java/bisq/httpapi/ApiTestHelper.java | 13 +++ .../java/bisq/httpapi/ContainerFactory.java | 84 ++++++++++++++++++ .../java/bisq/httpapi/RegexMatcher.java | 28 ++++++ .../java/bisq/httpapi/SwaggerIT.java | 61 +++++++++++++ .../java/bisq/httpapi/VersionEndpointIT.java | 56 ++++++++++++ .../bisq/httpapi/arquillian/CubeLogger.java | 30 +++++++ .../arquillian/CubeLoggerExtension.java | 11 +++ ...boss.arquillian.core.spi.LoadableExtension | 1 + .../testIntegration/resources/arquillian.xml | 13 +++ .../resources/logback-test.xml | 15 ++++ settings.gradle | 1 + 34 files changed, 1112 insertions(+), 23 deletions(-) create mode 100644 .dockerignore create mode 100644 http-api/.dockerignore create mode 100644 http-api/docker-compose-base.yml create mode 100644 http-api/docker-compose.yml create mode 100644 http-api/docker/dev/Dockerfile create mode 100755 http-api/docker/startApi.sh create mode 100644 http-api/src/main/java/bisq/httpapi/HttpApiModule.java create mode 100644 http-api/src/main/java/bisq/httpapi/app/HttpApiHeadlessApp.java create mode 100644 http-api/src/main/java/bisq/httpapi/app/HttpApiHeadlessModule.java create mode 100644 http-api/src/main/java/bisq/httpapi/app/HttpApiMain.java create mode 100644 http-api/src/main/java/bisq/httpapi/model/VersionDetails.java create mode 100644 http-api/src/main/java/bisq/httpapi/service/HttpApiInterfaceV1.java create mode 100644 http-api/src/main/java/bisq/httpapi/service/HttpApiServer.java create mode 100644 http-api/src/main/java/bisq/httpapi/service/endpoint/VersionEndpoint.java create mode 100644 http-api/src/main/resources/META-INF/custom-swagger-ui/index.html create mode 100644 http-api/src/main/resources/openapi-configuration.json create mode 100644 http-api/src/testIntegration/java/bisq/httpapi/ApiTestHelper.java create mode 100644 http-api/src/testIntegration/java/bisq/httpapi/ContainerFactory.java create mode 100644 http-api/src/testIntegration/java/bisq/httpapi/RegexMatcher.java create mode 100644 http-api/src/testIntegration/java/bisq/httpapi/SwaggerIT.java create mode 100644 http-api/src/testIntegration/java/bisq/httpapi/VersionEndpointIT.java create mode 100644 http-api/src/testIntegration/java/bisq/httpapi/arquillian/CubeLogger.java create mode 100644 http-api/src/testIntegration/java/bisq/httpapi/arquillian/CubeLoggerExtension.java create mode 100644 http-api/src/testIntegration/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension create mode 100644 http-api/src/testIntegration/resources/arquillian.xml create mode 100644 http-api/src/testIntegration/resources/logback-test.xml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000000..8a67891ccde --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +.git +.gitignore +.gitmodules +.dockerignore +.travis.yml +docker-compose.yml +docker-compose-base.yml +Dockerfile +.idea +*.iml +target +support +.gradle +build +out +doc + +http-api/docker/dev/Dockerfile +http-api/docker/prod/Dockerfile +http-api/.dockerignore +http-api/.editorconfig +http-api/.git +http-api/.gitignore +http-api/.travis.yml +http-api/support diff --git a/.travis.yml b/.travis.yml index 9a389a5e411..1b5eaee1152 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,29 @@ language: java jdk: openjdk10 -before_install: - grep -v '^#' assets/src/main/resources/META-INF/services/bisq.asset.Asset | sort --check --dictionary-order --ignore-case +sudo: required +services: +- docker +jobs: + include: + - stage: asset ordering + install: + script: grep -v '^#' assets/src/main/resources/META-INF/services/bisq.asset.Asset | sort --check --dictionary-order --ignore-case + - stage: build + - stage: integration + env: + - CUBE_LOGGER_ENABLE=true + install: cd http-api; docker-compose build; + script: ../gradlew testIntegration; +before_cache: +- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock +- rm -fr $HOME/.gradle/caches/*/plugin-resolution/ +cache: + directories: + - $HOME/.gradle/caches/ + - $HOME/.gradle/wrapper/ notifications: - slack: - on_success: change - on_failure: always - rooms: - - secure: EzlqWTBuhb3FCfApjUXaShWgsWOVDwMXI7ISMiVBkcl2VFISYs/lc/7Qjr7rdy4iqQOQXMcUPHdwMUp0diX+GxiSjLARjUpKIvNOPswZWBL+3Z1h28jyOwtHRviZbM1EU0BZROrr+ODyTNz2lf+L1iXTkpSvk50o5JAnAkumsPw= + slack: + on_success: change + on_failure: always + rooms: + - secure: EzlqWTBuhb3FCfApjUXaShWgsWOVDwMXI7ISMiVBkcl2VFISYs/lc/7Qjr7rdy4iqQOQXMcUPHdwMUp0diX+GxiSjLARjUpKIvNOPswZWBL+3Z1h28jyOwtHRviZbM1EU0BZROrr+ODyTNz2lf+L1iXTkpSvk50o5JAnAkumsPw= diff --git a/build.gradle b/build.gradle index ddf8b497bca..50f6a690a53 100644 --- a/build.gradle +++ b/build.gradle @@ -271,6 +271,85 @@ configure(project(':core')) { } +configure(project(':http-api')) { + + apply plugin: 'application' + + mainClassName = 'bisq.httpapi.app.HttpApiMain' + + dependencies { + compile project(':common') + compile project(':assets') + compile project(':core') + compile project(':p2p') + + compile 'org.eclipse.jetty:jetty-servlet:9.4.14.v20181114' + compile 'javax.activation:activation:1.1.1' + compile 'javax.ws.rs:javax.ws.rs-api:2.1' + compile 'org.glassfish.jersey.containers:jersey-container-servlet:2.27' + compile 'org.glassfish.jersey.inject:jersey-hk2:2.27' + compile 'org.glassfish.jersey.media:jersey-media-json-jackson:2.27' + compile 'org.glassfish.jersey.media:jersey-media-multipart:2.27' + compile 'org.glassfish.jersey.ext:jersey-bean-validation:2.27' + + compile 'io.swagger.core.v3:swagger-jaxrs2:2.0.6' + compile 'org.webjars:swagger-ui:3.20.1' + + compileOnly 'org.projectlombok:lombok:1.18.2' + annotationProcessor 'org.projectlombok:lombok:1.18.2' + + testCompile 'junit:junit:4.12' + testCompile('org.mockito:mockito-core:2.8.9') { + exclude(module: 'objenesis') + } + testCompileOnly 'org.projectlombok:lombok:1.18.2' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.2' + testCompile "junit:junit:4.12" + testCompile "org.mockito:mockito-core:2.7.5" + testCompile "com.github.javafaker:javafaker:0.14" + testCompile "org.arquillian.universe:arquillian-junit:1.2.0.1" + testCompile "org.arquillian.universe:arquillian-cube-docker:1.2.0.1" + testCompile "org.arquillian.cube:arquillian-cube-docker:1.15.3" + testCompile "io.rest-assured:rest-assured:3.0.2" + } + + sourceSets { + testIntegration { + java.srcDir 'src/testIntegration/java' + resources.srcDir 'src/testIntegration/resources' + compileClasspath += sourceSets.main.output + configurations.testRuntimeClasspath + runtimeClasspath += output + compileClasspath + } + } + + task testIntegration(type: Test) { + group = LifecycleBasePlugin.VERIFICATION_GROUP + description = 'Runs the integration tests.' + + maxHeapSize = '1024m' + + testClassesDir = sourceSets.testIntegration.output.classesDir + classpath = sourceSets.testIntegration.runtimeClasspath + + binResultsDir = file("$buildDir/http-api/integration-test-results/binary/testIntegration") + + reports { + html.destination = "$buildDir/http-api/reports/integration-test" + junitXml.destination = "$buildDir/http-api/integration-test-results" + } + + systemProperties = [ + CUBE_LOGGER_ENABLE: System.getenv('CUBE_LOGGER_ENABLE') + ] + + testLogging.showStandardStreams = true + testLogging.exceptionFormat = 'full' + + mustRunAfter tasks.test + } +} + + configure(project(':desktop')) { apply plugin: 'com.github.johnrengelman.shadow' apply plugin: 'witness' @@ -291,6 +370,7 @@ configure(project(':desktop')) { compile project(':p2p') compile project(':core') compile project(':common') + compile project(':http-api') compile 'org.controlsfx:controlsfx:8.0.6_20' compile 'org.reactfx:reactfx:2.0-M3' compile 'net.glxn:qrgen:1.3' diff --git a/core/src/main/java/bisq/core/app/AppOptionKeys.java b/core/src/main/java/bisq/core/app/AppOptionKeys.java index 6c44f7d4f81..ead7aea8c18 100644 --- a/core/src/main/java/bisq/core/app/AppOptionKeys.java +++ b/core/src/main/java/bisq/core/app/AppOptionKeys.java @@ -29,4 +29,7 @@ public class AppOptionKeys { public static final String IGNORE_DEV_MSG_KEY = "ignoreDevMsg"; public static final String USE_DEV_PRIVILEGE_KEYS = "useDevPrivilegeKeys"; public static final String REFERRAL_ID = "referralId"; + public static final String HTTP_API_EXPERIMENTAL_FEATURES_ENABLED = "enableHttpApiExperimentalFeatures"; + public static final String HTTP_API_HOST = "httpApiHost"; + public static final String HTTP_API_PORT = "httpApiPort"; } diff --git a/core/src/main/java/bisq/core/app/BisqEnvironment.java b/core/src/main/java/bisq/core/app/BisqEnvironment.java index d40e3bcc232..59456278371 100644 --- a/core/src/main/java/bisq/core/app/BisqEnvironment.java +++ b/core/src/main/java/bisq/core/app/BisqEnvironment.java @@ -208,6 +208,13 @@ public static boolean isDaoActivated(Environment environment) { protected final boolean externalTorUseSafeCookieAuthentication, torStreamIsolation; + @Getter + protected final String httpApiHost; + @Getter + protected final Integer httpApiPort; + @Getter + protected boolean httpApiExperimentalFeaturesEnabled; + public BisqEnvironment(OptionSet options) { this(new JOptCommandLinePropertySource(BISQ_COMMANDLINE_PROPERTY_SOURCE_NAME, checkNotNull( options))); @@ -249,6 +256,13 @@ public BisqEnvironment(PropertySource commandLineProperties) { referralId = commandLineProperties.containsProperty(AppOptionKeys.REFERRAL_ID) ? (String) commandLineProperties.getProperty(AppOptionKeys.REFERRAL_ID) : ""; + httpApiExperimentalFeaturesEnabled = commandLineProperties.containsProperty(AppOptionKeys.HTTP_API_EXPERIMENTAL_FEATURES_ENABLED); + httpApiHost = commandLineProperties.containsProperty(AppOptionKeys.HTTP_API_HOST) ? + (String) commandLineProperties.getProperty(AppOptionKeys.HTTP_API_HOST) : + "127.0.0.1"; + httpApiPort = Integer.parseInt(commandLineProperties.containsProperty(AppOptionKeys.HTTP_API_PORT) ? + (String) commandLineProperties.getProperty(AppOptionKeys.HTTP_API_PORT) : + "8080"); useDevMode = commandLineProperties.containsProperty(CommonOptionKeys.USE_DEV_MODE) ? (String) commandLineProperties.getProperty(CommonOptionKeys.USE_DEV_MODE) : ""; diff --git a/core/src/main/java/bisq/core/app/BisqExecutable.java b/core/src/main/java/bisq/core/app/BisqExecutable.java index 91b6b1e7307..9ccaf0b7ae6 100644 --- a/core/src/main/java/bisq/core/app/BisqExecutable.java +++ b/core/src/main/java/bisq/core/app/BisqExecutable.java @@ -458,6 +458,15 @@ protected void customizeOptionParsing(OptionParser parser) { "Optional Referral ID (e.g. for API users or pro market makers)") .withRequiredArg(); + parser.accepts(AppOptionKeys.HTTP_API_EXPERIMENTAL_FEATURES_ENABLED, "Enable experimental features of HTTP API (disabled by default)"); + parser.accepts(AppOptionKeys.HTTP_API_HOST, "Optional HTTP API host") + .withRequiredArg() + .defaultsTo("127.0.0.1"); + parser.accepts(AppOptionKeys.HTTP_API_PORT, "Optional HTTP API port") + .withRequiredArg() + .ofType(int.class) + .defaultsTo(8080); + parser.accepts(CommonOptionKeys.USE_DEV_MODE, format("Enables dev mode which is used for convenience for developer testing (default: %s)", "false")) .withRequiredArg() diff --git a/desktop/src/main/java/bisq/desktop/app/BisqAppMain.java b/desktop/src/main/java/bisq/desktop/app/BisqAppMain.java index a2494dfbdb1..fc34fbe8a7d 100644 --- a/desktop/src/main/java/bisq/desktop/app/BisqAppMain.java +++ b/desktop/src/main/java/bisq/desktop/app/BisqAppMain.java @@ -36,6 +36,10 @@ import lombok.extern.slf4j.Slf4j; + + +import bisq.httpapi.service.HttpApiServer; + @Slf4j public class BisqAppMain extends BisqExecutable { private BisqApp application; @@ -44,8 +48,6 @@ public BisqAppMain() { super("Bisq Desktop", "bisq-desktop", Version.VERSION); } - /* @Nullable - private BisqHttpApiServer bisqHttpApiServer;*/ /* @Nullable private BisqGrpcServer bisqGrpcServer; */ @@ -64,7 +66,6 @@ public void onSetupComplete() { log.debug("onSetupComplete"); } - /////////////////////////////////////////////////////////////////////////////////////////// // First synchronous execution tasks /////////////////////////////////////////////////////////////////////////////////////////// @@ -135,9 +136,9 @@ protected void startApplication() { protected void onApplicationStarted() { super.onApplicationStarted(); - /* if (runWithHttpApi()) { - bisqHttpApiServer = new BisqHttpApiServer(); - }*/ + if (runWithHttpApi()) { + injector.getInstance(HttpApiServer.class).startServer(); + } /* if (runWithGrpcApi()) { bisqGrpcServer = new BisqGrpcServer(); diff --git a/gradle/witness/gradle-witness.gradle b/gradle/witness/gradle-witness.gradle index 75c8d891cc8..eec31615612 100644 --- a/gradle/witness/gradle-witness.gradle +++ b/gradle/witness/gradle-witness.gradle @@ -29,9 +29,18 @@ dependencyVerification { 'org.fxmisc.easybind:easybind:666af296dda6de68751668a62661571b5238ac6f1c07c8a204fc6f902b222aaf', 'network.bisq.btcd-cli4j:btcd-cli4j-daemon:c007116da1b0145ddee64bb3a54fef60d58ce5c3dcf27773f39471117be8f132', 'network.bisq.btcd-cli4j:btcd-cli4j-core:b1d0525f3629bad358ad4a40ea3be998220110331d4b9d24e76d7894e563a595', - 'com.fasterxml.jackson.core:jackson-databind:fcf3c2b0c332f5f54604f7e27fa7ee502378a2cc5df6a944bbfae391872c32ff', - 'com.fasterxml.jackson.core:jackson-core:39a74610521d7fb9eb3f437bb8739bbf47f6435be12d17bf954c731a0c6352bb', - 'com.fasterxml.jackson.core:jackson-annotations:2566b3a6662afa3c6af4f5b25006cb46be2efc68f1b5116291d6998a8cdf7ed3', + 'org.glassfish.jersey.media:jersey-media-json-jackson:815a783428d87e3f74591c6a9e4fd9c4bf37f5492e4c574b0a3e26a731dabc86', + 'io.swagger.core.v3:swagger-jaxrs2:f4de63695032f11df9664e92ca05284a5c2f484cf02cd53222513fd2ab4c8d32', + 'io.swagger.core.v3:swagger-integration:470dd943148b0ce90747af0583fb1a9c6db5979f845970353995c634bb124172', + 'io.swagger.core.v3:swagger-core:ef8bbaa4bc5643e17dafaaab137c43fb55c47e34287c909546f30f44ed332be0', + 'com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:26af65ee8a17c6751d51f90e241dd3974df0ba85bc6ba48709e58b509c3219e6', + 'com.fasterxml.jackson.module:jackson-module-jaxb-annotations:eed08585b2a9b6d64f8dba5ab813813dde1a8df5e7f7e744a3a70d83925bcfc2', + 'com.fasterxml.jackson.jaxrs:jackson-jaxrs-base:d81b4b71778bdca92f4c8b66db08d3e1e3b3fa5b0e4fe349d2f8b59a310cbdd2', + 'com.fasterxml.jackson.core:jackson-databind:0fb4e079c118e752cc94c15ad22e6782b0dfc5dc09145f4813fb39d82e686047', + 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:adc1c495e9c7286bfa1d861ca07e06c8d0980057ad981b48b04f68eb8dcade2c', + 'com.fasterxml.jackson.core:jackson-core:a2bebaa325ad25455b02149c67e6052367a7d7fc1ce77de000eed284a5214eac', + 'io.swagger.core.v3:swagger-models:01453452b0715f6ddd5ff52f82b7a2646d0753360f600fd14bb734853c3765a2', + 'com.fasterxml.jackson.core:jackson-annotations:38a0e450049f643570adac99888aa3480ec2de385790a7096908bf43bfc085d6', 'com.google.protobuf:protobuf-java:b5e2d91812d183c9f053ffeebcbcda034d4de6679521940a19064714966c2cd4', 'com.google.code.gson:gson:2d43eb5ea9e133d2ee2405cc14f5ee08951b8361302fdd93494a3a997b508d32', 'com.googlecode.json-simple:json-simple:4e69696892b88b41c55d49ab2fdcc21eead92bf54acc588c0050596c3b75199c', @@ -39,9 +48,10 @@ dependencyVerification { 'ch.qos.logback:logback-classic:e66efc674e94837344bc5b748ff510c37a44eeff86cbfdbf9e714ef2eb374013', 'network.bisq.libdohj:libdohj-core:b89d2a6ad6a5aff1fccf2d4e5f7cc8c31991746e61913bcec3e999c2b0d7c954', 'com.github.bisq-network.bitcoinj:bitcoinj-core:d148d9577cf96540f7f5367011f7626ff9c9f148f0bf903b541740d480652969', - 'org.slf4j:slf4j-api:3a4cd4969015f3beb4b5b4d81dbafc01765fb60b8a439955ca64d8476fef553e', + 'org.slf4j:slf4j-api:18c4a0095d5c1da6b817592e767bb23d29dd2f560ad74df75ff3961dbde25b79', 'ch.qos.logback:logback-core:4cd46fa17d77057b39160058df2f21ebbc2aded51d0edcc25d2c1cecc042a005', 'com.google.code.findbugs:jsr305:766ad2a0783f2687962c8ad74ceecc38a28b9f72a2d085ee438b7813e928d0c7', + 'org.reflections:reflections:cca88428f8a8919df885105833d45ff07bd26f985f96ee55690551216b58b4a1', 'com.google.guava:guava:36a666e3b71ae7f0f0dca23654b67e086e6c93d192f60ba5dfd5519db6c288c8', 'com.google.inject:guice:9b9df27a5b8c7864112b4137fd92b36c3f1395bfe57be42fedf2f520ead1a93e', 'com.github.JesusMcCloud.netlayer:tor:75ca62a08621ea532b42ae6b014d9604a856ed572ffc8d2c15c281df817702ba', @@ -52,8 +62,22 @@ dependencyVerification { 'org.jetbrains:annotations:ace2a10dc8e2d5fd34925ecac03e4988b2c0f851650c94b8cef49ba1bd111478', 'org.bouncycastle:bcpg-jdk15on:de3355b821fc81dd32e1f3f560d5b3eca1c678fd2400011d0bfc69fb91bcde85', 'commons-io:commons-io:cc6a41dc3eaacc9e440a6bd0d2890b20d36b4ee408fe2d67122f328bb6e01581', - 'org.apache.commons:commons-lang3:734c8356420cc8e30c795d64fd1fcd5d44ea9d90342a2cc3262c5158fbc6d98b', + 'org.apache.commons:commons-lang3:6e8dc31e046508d9953c96534edf0c2e0bfe6f468966b5b842b3f87e43b6a847', 'org.bouncycastle:bcprov-jdk15on:963e1ee14f808ffb99897d848ddcdb28fa91ddda867eb18d303e82728f878349', + 'org.eclipse.jetty:jetty-servlet:fbcd7cc44b91a7f318930b237ad4fdeae79fe84ebeee6007baef01724a39e735', + 'javax.activation:activation:ae475120e9fcd99b4b00b38329bd61cdc5eb754eee03fe66c01f50e137724f99', + 'org.glassfish.jersey.containers:jersey-container-servlet:40349db8dabf6327a01ad59eaff172bd9a5f8927b2411bcdc59ceb05ce7731c1', + 'org.glassfish.jersey.ext:jersey-bean-validation:9dc23bd60a6ff1b8ce3f436b1dec959f82a1a643b7a981f4462700aae432c16d', + 'org.glassfish.jersey.containers:jersey-container-servlet-core:39e9fee46f5c6b5d4e49dc03f54741671bd4261090c5f7b5c72541a232873946', + 'org.glassfish.jersey.inject:jersey-hk2:634a2790f08c2f33feb78586b22a23005a2f8aa483c316ae2435729be0943968', + 'org.glassfish.jersey.media:jersey-media-multipart:08b303988e99546364283c63da5aa2d79c7c823f7b0d1ca5deabe66fbbb6374e', + 'org.glassfish.jersey.core:jersey-server:45a2e1e87566cb9808953d1f5ce0b4d99ede51be4a0f22ed92a7ceda7ba9417e', + 'org.glassfish.jersey.core:jersey-client:aba407bda94df54f590041b4cde5f2fa31db45bd8b4cf7575af48c1f8f81bb04', + 'org.glassfish.jersey.media:jersey-media-jaxb:b295e0d7ca93dbb084abd22a01ae7f54d5ffa244dd4b3ce1a6792eda148e76b2', + 'org.glassfish.jersey.core:jersey-common:9a9578c6dac52b96195a614150f696d455db6b6d267a645c3120a4d0ee495789', + 'org.glassfish.jersey.ext:jersey-entity-filtering:529b7ee7830441cffe98851b1e6edc0edd30e8b066052999daa5de63c56302b2', + 'javax.ws.rs:javax.ws.rs-api:1a4295889416c6972addcd425dfeeee6e6ede110e8b2dc8b49044e9b400ad5db', + 'org.webjars:swagger-ui:d6aa5d51493c016e95559452a911ffef739170edc063ff3a3e15ba76d0a40fa5', 'com.google.zxing:javase:0ec23e2ec12664ddd6347c8920ad647bb3b9da290f897a88516014b56cc77eb9', 'com.nativelibs4java:bridj:101bcd9b6637e6bc16e56deb3daefba62b1f5e8e9e37e1b3e56e3b5860d659cf', 'com.github.JesusMcCloud.tor-binary:tor-binary-macos:d143904dee93952576b12afb3c255ffce1b4eb0f8c85b0078c753b5f57fa1d07', @@ -64,19 +88,43 @@ dependencyVerification { 'org.apache.httpcomponents:httpcore:d7f853dee87680b07293d30855b39b9eb56c1297bd16ff1cd6f19ddb8fa745fb', 'commons-codec:commons-codec:ad19d2601c3abf0b946b5c3a4113e226a8c1e3305e395b90013b78dd94a723ce', 'commons-logging:commons-logging:daddea1ea0be0f56978ab3006b8ac92834afeefbd9b7e4e6316fca57df0fa636', + 'org.glassfish.hk2:hk2-locator:2a3766079d1cd21e715e4fd75e8189ebd58f9bf852885f1679253f0a27039b72', + 'org.glassfish.hk2:hk2-api:4d328e5b1cb5e8dcf3f97e1348d960f439e597009aa9d994dd5325bcef367908', + 'org.glassfish.hk2:hk2-utils:5edb176086ad0be1d4abbc0a1d26d519d088a01ee24f9ee064c657895a86e3ee', 'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff', 'aopalliance:aopalliance:0addec670fedcd3f113c5c8091d783280d23f75e3acb841b61a9cdb079376a08', 'com.lambdaworks:scrypt:9a82d218099fb14c10c0e86e7eefeebd8c104de920acdc47b8b4b7a686fb73b4', + 'com.madgag.spongycastle:core:8d6240b974b0aca4d3da9c7dd44d42339d8a374358aca5fc98e50a995764511f', + 'net.jcip:jcip-annotations:be5805392060c71474bf6c9a67a099471274d30b83eef84bfc4e0889a4f1dcc0', + 'org.bitcoinj:orchid:f836325cfa0466a011cb755c9b0fee6368487a2352eb45f4306ad9e4c18de080', + 'com.squareup.okhttp:okhttp:b4c943138fcef2bcc9d2006b2250c4aabbedeafc5947ed7c0af7fd103ceb2707', + 'org.eclipse.jetty:jetty-security:c4102cdbcbf6becec08b9ab4afbd9979c3f120e8cf45fbdf6be6c3940fe55bf1', + 'org.jvnet.mimepull:mimepull:2d1ee56aa89837ba9ea55431542e7939fa9d425552c2e6c8ddfb3b77877721b7', + 'org.glassfish.hk2.external:javax.inject:3bcf096beb918c9586be829190903090a21ac40513c1401e1b986e6030addc98', + 'org.hibernate:hibernate-validator:7f9300345436349396944fc9347437d862f999abd563ebd212291a44ff66e41b', + 'javax.validation:validation-api:f39d7ba7253e35f5ac48081ec1bc28c5df9b32ac4b7db20853e5a8e76bf7b0ed', + 'org.glassfish.web:javax.el:787e7e247da8008c699bafd8e086ccae13e6f3cac3c07ca1c698e44f917b42de', + 'javax.el:javax.el-api:5fd94735743ed06252c83158a24c290fcbf94b3f599b1bcec3bdc8c80979bed7', + 'org.javassist:javassist:59531c00f3e3aa1ff48b3a8cf4ead47d203ab0e2fd9e0ad401f764e05947e252', + 'io.swagger.core.v3:swagger-annotations:36ee5908faa140104b4c19f9273b4813f70f7d8486074119a7b83360fd4a4efb', 'com.google.zxing:core:11aae8fd974ab25faa8208be50468eb12349cd239e93e7c797377fa13e381729', 'com.github.JesusMcCloud.tor-binary:tor-binary-geoip:7340d4a0007b822b2f149e7360cd22443a5976676754bfaad0b8c48858475d5f', 'com.github.JesusMcCloud:jtorctl:b8be77613eeba899abff797c3cb9159b96361b2f880a3ebcc2280c027b0758e3', 'org.apache.commons:commons-compress:5f2df1e467825e4cac5996d44890c4201c000b43c0b23cffc0782d28a0beb9b0', 'org.tukaani:xz:a594643d73cc01928cf6ca5ce100e094ea9d73af760a5d4fb6b75fa673ecec96', - 'com.madgag.spongycastle:core:8d6240b974b0aca4d3da9c7dd44d42339d8a374358aca5fc98e50a995764511f', - 'net.jcip:jcip-annotations:be5805392060c71474bf6c9a67a099471274d30b83eef84bfc4e0889a4f1dcc0', - 'org.bitcoinj:orchid:f836325cfa0466a011cb755c9b0fee6368487a2352eb45f4306ad9e4c18de080', - 'com.squareup.okhttp:okhttp:b4c943138fcef2bcc9d2006b2250c4aabbedeafc5947ed7c0af7fd103ceb2707', - 'org.jetbrains.kotlin:kotlin-stdlib-common:4b161ef619eee0d1a49b1c4f0c4a8e46f4e342573efd8e0106a765f47475fe39', 'com.squareup.okio:okio:114bdc1f47338a68bcbc95abf2f5cdc72beeec91812f2fcd7b521c1937876266', + 'org.eclipse.jetty:jetty-server:a922f870f891f6ff4e3503b4e32ac80efb999e9d373ba1f397d055e1f34c8c4a', + 'javax.annotation:javax.annotation-api:5909b396ca3a2be10d0eea32c74ef78d816e1b4ead21de1d78de1f890d033e04', + 'org.glassfish.hk2:osgi-resource-locator:775003be577e8806f51b6e442be1033d83be2cb2207227b349be0bf16e6c0843', + 'org.glassfish.hk2.external:aopalliance-repackaged:669869a9d7e98fcea34580de250db54531550487d03571f26b9592e712897423', + 'org.jboss.logging:jboss-logging:6813931fe607469989f76a73a22515d2489dcd8b6be9fc147093a9cec995f822', + 'com.fasterxml:classmate:1a381660e2f27912e2c421a70bf816a1739e0475190a8efefbcdd00d89216887', + 'org.jetbrains.kotlin:kotlin-stdlib-common:4b161ef619eee0d1a49b1c4f0c4a8e46f4e342573efd8e0106a765f47475fe39', + 'javax.servlet:javax.servlet-api:af456b2dd41c4e82cf54f3e743bc678973d9fe35bd4d3071fa05c7e5333b8482', + 'org.eclipse.jetty:jetty-http:964795275e9ea340e302845630dd441d0c4977d99c990f28537d6e834260d64f', + 'org.eclipse.jetty:jetty-io:3710e8c88f99c8047ad38e4163715c1e63026f3fa586fa7727cf81b54dc420d5', + 'javax.xml.bind:jaxb-api:883007989d373d19f352ba9792b25dec21dc7d0e205a710a93a3815101bb3d03', + 'org.eclipse.jetty:jetty-util:02469929c448d4a3197dc69c5a6b9296bd37d12bcafd2b576ec8fe47ee42ef8f', + 'org.yaml:snakeyaml:81bf4c29d0275dace75fadb5febf5384553422816256023efa83b2b15a9ced60', ] } diff --git a/http-api/.dockerignore b/http-api/.dockerignore new file mode 100644 index 00000000000..686ea619974 --- /dev/null +++ b/http-api/.dockerignore @@ -0,0 +1,17 @@ +.git +.gitignore +.dockerignore +docker-compose.yml +docker-compose-base.yml +Dockerfile +docker/dev/Dockerfile +docker/prod/Dockerfile + +.idea +*.iml + +target +support + +build +out diff --git a/http-api/docker-compose-base.yml b/http-api/docker-compose-base.yml new file mode 100644 index 00000000000..07ef4d5e12d --- /dev/null +++ b/http-api/docker-compose-base.yml @@ -0,0 +1,34 @@ +version: '2.1' + +services: + api: + build: + context: .. + dockerfile: http-api/docker/dev/Dockerfile + image: bisq/http-api + environment: + - LOG_LEVEL=debug + - USE_LOCALHOST_FOR_P2P=true + - USE_DEV_PRIVILEGE_KEYS=true + - SEED_NODES=seed:8000 + - BTC_NODES=bitcoin:18444 + - BASE_CURRENCY_NETWORK=BTC_REGTEST + - BITCOIN_REGTEST_HOST=NONE + - HTTP_API_HOST=0.0.0.0 + - ENABLE_HTTP_API_EXPERIMENTAL_FEATURES=true + seed: + image: bisq/seednode + environment: + - MY_ADDRESS=seed:8000 + - SEED_NODES=seed:8000 + - BTC_NODES=bitcoin:18444 + - USE_LOCALHOST_FOR_P2P=true + - BASE_CURRENCY_NETWORK=BTC_REGTEST + - BITCOIN_REGTEST_HOST=NONE + bitcoin: + image: kylemanna/bitcoind:latest + command: -logips -rpcallowip=::/0 -regtest -printtoconsole + environment: + - DISABLEWALLET=0 + - RPCUSER=user + - RPCPASSWORD=password diff --git a/http-api/docker-compose.yml b/http-api/docker-compose.yml new file mode 100644 index 00000000000..2305ff48f75 --- /dev/null +++ b/http-api/docker-compose.yml @@ -0,0 +1,51 @@ +version: '2.1' + +services: + alice: + extends: + file: docker-compose-base.yml + service: api + ports: + - 8080:8080 + environment: + - NODE_PORT=8003 + links: + - seed + - bitcoin + bob: + extends: + file: docker-compose-base.yml + service: api + ports: + - 8081:8080 + environment: + - NODE_PORT=8004 + links: + - seed + - bitcoin + arbitrator: + extends: + file: docker-compose-base.yml + service: api + ports: + - 8082:8080 + environment: + - NODE_PORT=8005 + links: + - seed + - bitcoin + seed: + extends: + file: docker-compose-base.yml + service: seed + ports: + - 8000:8000 + links: + - bitcoin + bitcoin: + extends: + file: docker-compose-base.yml + service: bitcoin + ports: +# If it is default regtest port (18444) then bisq ignores btcNodes param and uses localhost + - 18445:18444 diff --git a/http-api/docker/dev/Dockerfile b/http-api/docker/dev/Dockerfile new file mode 100644 index 00000000000..26994090ab0 --- /dev/null +++ b/http-api/docker/dev/Dockerfile @@ -0,0 +1,29 @@ +FROM openjdk:10-jdk + +RUN apt-get update && apt-get install -y --no-install-recommends \ + openjfx && rm -rf /var/lib/apt/lists/* + +WORKDIR /bisq/http-api + +#ENV HTTP_API_HOST= +#ENV HTTP_API_PORT= +ENV LANG=en_US + +CMD ./docker/startApi.sh + +#Fetch gradle and dependencies +COPY gradle /bisq/gradle/ +COPY gradlew build.gradle gradle.properties settings.gradle /bisq/ +RUN ../gradlew --no-daemon -v + +COPY assets /bisq/assets/ +COPY common /bisq/common/ +COPY core /bisq/core/ +COPY p2p /bisq/p2p/ +COPY pricenode /bisq/pricenode/ +RUN ../gradlew --no-daemon build -x test + +COPY http-api /bisq/http-api + +#Compile sources to speed up startup +RUN ../gradlew --no-daemon --offline compileJava -x test diff --git a/http-api/docker/startApi.sh b/http-api/docker/startApi.sh new file mode 100755 index 00000000000..969e436f117 --- /dev/null +++ b/http-api/docker/startApi.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +IP=`ip -4 -o addr show eth0 | sed 's/.*inet \([0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\).*/\1/'` +ARGS="--myAddress $IP:${NODE_PORT:-9999}" +if [ ! -z "$APP_NAME" ]; then + ARGS="$ARGS --appName=$APP_NAME" +fi +if [ ! -z "$NODE_PORT" ]; then + ARGS="$ARGS --nodePort=$NODE_PORT" +fi +if [ ! -z "$USE_LOCALHOST_FOR_P2P" ]; then + ARGS="$ARGS --useLocalhostForP2P=$USE_LOCALHOST_FOR_P2P" +fi +if [ ! -z "$SEED_NODES" ]; then + ARGS="$ARGS --seedNodes=$SEED_NODES" +fi +if [ ! -z "$BTC_NODES" ]; then + ARGS="$ARGS --btcNodes=$BTC_NODES" +fi +if [ ! -z "$BITCOIN_REGTEST_HOST" ]; then + ARGS="$ARGS --bitcoinRegtestHost=$BITCOIN_REGTEST_HOST" +fi +if [ ! -z "$BASE_CURRENCY_NETWORK" ]; then + ARGS="$ARGS --baseCurrencyNetwork=$BASE_CURRENCY_NETWORK" +fi +if [ ! -z "$LOG_LEVEL" ]; then + ARGS="$ARGS --logLevel=$LOG_LEVEL" +fi +if [ ! -z "$USE_DEV_PRIVILEGE_KEYS" ]; then + ARGS="$ARGS --useDevPrivilegeKeys=$USE_DEV_PRIVILEGE_KEYS" +fi +if [ "$ENABLE_HTTP_API_EXPERIMENTAL_FEATURES" == "true" ]; then + ARGS="$ARGS --enableHttpApiExperimentalFeatures" +fi +if [ ! -z "$HTTP_API_PORT" ]; then + ARGS="$ARGS --httpApiPort=$HTTP_API_PORT" +fi +if [ ! -z "$HTTP_API_HOST" ]; then + ARGS="$ARGS --httpApiHost=$HTTP_API_HOST" +fi + +echo ../gradlew run --no-daemon --args "foo $ARGS" +../gradlew run --no-daemon --args "foo $ARGS" diff --git a/http-api/src/main/java/bisq/httpapi/HttpApiModule.java b/http-api/src/main/java/bisq/httpapi/HttpApiModule.java new file mode 100644 index 00000000000..a8f591059e1 --- /dev/null +++ b/http-api/src/main/java/bisq/httpapi/HttpApiModule.java @@ -0,0 +1,51 @@ +/* + * This file is part of Bitsquare. + * + * Bitsquare is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bitsquare is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bitsquare. If not, see . + */ + +package bisq.httpapi; + +import bisq.core.app.AppOptionKeys; + +import bisq.common.app.AppModule; + +import org.springframework.core.env.Environment; + +import com.google.inject.Singleton; +import com.google.inject.name.Names; + + + +import bisq.httpapi.service.HttpApiServer; +import bisq.httpapi.service.endpoint.VersionEndpoint; + +public class HttpApiModule extends AppModule { + + public HttpApiModule(Environment environment) { + super(environment); + } + + @Override + protected void configure() { + bind(HttpApiServer.class).in(Singleton.class); + bind(VersionEndpoint.class).in(Singleton.class); + + String httpApiHost = environment.getProperty(AppOptionKeys.HTTP_API_HOST, String.class, "127.0.0.1"); + bind(String.class).annotatedWith(Names.named(AppOptionKeys.HTTP_API_HOST)).toInstance(httpApiHost); + + Integer httpApiPort = Integer.valueOf(environment.getProperty(AppOptionKeys.HTTP_API_PORT, String.class, "8080")); + bind(Integer.class).annotatedWith(Names.named(AppOptionKeys.HTTP_API_PORT)).toInstance(httpApiPort); + } +} diff --git a/http-api/src/main/java/bisq/httpapi/app/HttpApiHeadlessApp.java b/http-api/src/main/java/bisq/httpapi/app/HttpApiHeadlessApp.java new file mode 100644 index 00000000000..bbdbaa1d118 --- /dev/null +++ b/http-api/src/main/java/bisq/httpapi/app/HttpApiHeadlessApp.java @@ -0,0 +1,15 @@ +package bisq.httpapi.app; + +import bisq.core.app.BisqHeadlessApp; + +import bisq.common.setup.UncaughtExceptionHandler; + +import lombok.extern.slf4j.Slf4j; + +/** + * BisqHeadlessApp implementation for HttpApi. + * This is only used in case of the headless version to startup Bisq. + */ +@Slf4j +class HttpApiHeadlessApp extends BisqHeadlessApp implements UncaughtExceptionHandler { +} diff --git a/http-api/src/main/java/bisq/httpapi/app/HttpApiHeadlessModule.java b/http-api/src/main/java/bisq/httpapi/app/HttpApiHeadlessModule.java new file mode 100644 index 00000000000..74341ca5c73 --- /dev/null +++ b/http-api/src/main/java/bisq/httpapi/app/HttpApiHeadlessModule.java @@ -0,0 +1,44 @@ +/* + * This file is part of Bitsquare. + * + * Bitsquare is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bitsquare is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bitsquare. If not, see . + */ + +package bisq.httpapi.app; + +import bisq.core.CoreModule; + +import bisq.common.app.AppModule; + +import org.springframework.core.env.Environment; + + + +import bisq.httpapi.HttpApiModule; + +/** + * Used in case of the headless version. + */ +public class HttpApiHeadlessModule extends AppModule { + + public HttpApiHeadlessModule(Environment environment) { + super(environment); + } + + @Override + protected void configure() { + install(new CoreModule(environment)); + install(new HttpApiModule(environment)); + } +} diff --git a/http-api/src/main/java/bisq/httpapi/app/HttpApiMain.java b/http-api/src/main/java/bisq/httpapi/app/HttpApiMain.java new file mode 100644 index 00000000000..3fc51d3db09 --- /dev/null +++ b/http-api/src/main/java/bisq/httpapi/app/HttpApiMain.java @@ -0,0 +1,69 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.httpapi.app; + +import bisq.core.app.BisqExecutable; +import bisq.core.app.BisqHeadlessAppMain; + +import bisq.common.UserThread; +import bisq.common.app.AppModule; +import bisq.common.setup.CommonSetup; + +import lombok.extern.slf4j.Slf4j; + + + +import bisq.httpapi.service.HttpApiServer; + +/** + * Main class for headless version. + */ +@Slf4j +public class HttpApiMain extends BisqHeadlessAppMain { + + public static void main(String[] args) throws Exception { + if (BisqExecutable.setupInitialOptionParser(args)) { + // For some reason the JavaFX launch process results in us losing the thread context class loader: reset it. + // In order to work around a bug in JavaFX 8u25 and below, you must include the following code as the first line of your realMain method: + Thread.currentThread().setContextClassLoader(HttpApiMain.class.getClassLoader()); + + new HttpApiMain().execute(args); + } + } + + @Override + protected void launchApplication() { + headlessApp = new HttpApiHeadlessApp(); + CommonSetup.setup(HttpApiMain.this.headlessApp); + + UserThread.execute(this::onApplicationLaunched); + } + + @Override + protected AppModule getModule() { + return new HttpApiHeadlessModule(bisqEnvironment); + } + + @Override + public void onSetupComplete() { + log.info("onSetupComplete"); + + HttpApiServer httpApiServer = injector.getInstance(HttpApiServer.class); + httpApiServer.startServer(); + } +} diff --git a/http-api/src/main/java/bisq/httpapi/model/VersionDetails.java b/http-api/src/main/java/bisq/httpapi/model/VersionDetails.java new file mode 100644 index 00000000000..d3023dbe6c5 --- /dev/null +++ b/http-api/src/main/java/bisq/httpapi/model/VersionDetails.java @@ -0,0 +1,11 @@ +package bisq.httpapi.model; + +public class VersionDetails { + + public String application; + public int network; + public int p2PMessage; + public int localDB; + public int tradeProtocol; + +} diff --git a/http-api/src/main/java/bisq/httpapi/service/HttpApiInterfaceV1.java b/http-api/src/main/java/bisq/httpapi/service/HttpApiInterfaceV1.java new file mode 100644 index 00000000000..e4f00921a10 --- /dev/null +++ b/http-api/src/main/java/bisq/httpapi/service/HttpApiInterfaceV1.java @@ -0,0 +1,33 @@ +package bisq.httpapi.service; + +import javax.inject.Inject; + + + +import bisq.httpapi.service.endpoint.VersionEndpoint; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.tags.Tag; +import javax.ws.rs.Path; + +@OpenAPIDefinition( + info = @Info(version = "0.0.1", title = "Bisq HTTP API"), + tags = { + @Tag(name = "version") + } +) +@Path("/api/v1") +public class HttpApiInterfaceV1 { + private final VersionEndpoint versionEndpoint; + + @Inject + public HttpApiInterfaceV1(VersionEndpoint versionEndpoint) { + this.versionEndpoint = versionEndpoint; + } + + @Path("version") + public VersionEndpoint getVersionEndpoint() { + return versionEndpoint; + } + +} diff --git a/http-api/src/main/java/bisq/httpapi/service/HttpApiServer.java b/http-api/src/main/java/bisq/httpapi/service/HttpApiServer.java new file mode 100644 index 00000000000..00af6a95922 --- /dev/null +++ b/http-api/src/main/java/bisq/httpapi/service/HttpApiServer.java @@ -0,0 +1,86 @@ +package bisq.httpapi.service; + +import bisq.core.app.BisqEnvironment; +import bisq.core.btc.wallet.BtcWalletService; + +import javax.inject.Inject; + +import java.net.InetSocketAddress; + +import lombok.extern.slf4j.Slf4j; + + + +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.Slf4jRequestLog; +import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.server.handler.ContextHandlerCollection; +import org.eclipse.jetty.server.handler.ResourceHandler; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.glassfish.jersey.media.multipart.MultiPartFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.servlet.ServletContainer; + +@SuppressWarnings("Duplicates") +@Slf4j +public class HttpApiServer { + private final BtcWalletService walletService; + private final HttpApiInterfaceV1 httpApiInterfaceV1; + private final BisqEnvironment bisqEnvironment; + + + @Inject + public HttpApiServer(BtcWalletService walletService, HttpApiInterfaceV1 httpApiInterfaceV1, + BisqEnvironment bisqEnvironment) { + this.walletService = walletService; + this.httpApiInterfaceV1 = httpApiInterfaceV1; + this.bisqEnvironment = bisqEnvironment; + } + + private ContextHandler buildAPIHandler() { + ResourceConfig resourceConfig = new ResourceConfig(); + resourceConfig.register(MultiPartFeature.class); + resourceConfig.register(httpApiInterfaceV1); + resourceConfig.packages("io.swagger.v3.jaxrs2.integration.resources"); + ServletContextHandler servletContextHandler = new ServletContextHandler(ServletContextHandler.NO_SESSIONS | ServletContextHandler.NO_SECURITY); + servletContextHandler.setContextPath("/"); + servletContextHandler.addServlet(new ServletHolder(new ServletContainer(resourceConfig)), "/*"); + return servletContextHandler; + } + + private ContextHandler buildSwaggerUIOverrideHandler() throws Exception { + ResourceHandler swaggerUIResourceHandler = new ResourceHandler(); + swaggerUIResourceHandler.setResourceBase(getClass().getClassLoader().getResource("META-INF/custom-swagger-ui").toURI().toString()); + ContextHandler swaggerUIContext = new ContextHandler(); + swaggerUIContext.setContextPath("/docs"); + swaggerUIContext.setHandler(swaggerUIResourceHandler); + return swaggerUIContext; + } + + private ContextHandler buildSwaggerUIHandler() throws Exception { + ResourceHandler swaggerUIResourceHandler = new ResourceHandler(); + swaggerUIResourceHandler.setResourceBase(getClass().getClassLoader().getResource("META-INF/resources/webjars/swagger-ui/3.20.1").toURI().toString()); + ContextHandler swaggerUIContext = new ContextHandler(); + swaggerUIContext.setContextPath("/docs"); + swaggerUIContext.setHandler(swaggerUIResourceHandler); + return swaggerUIContext; + } + + public void startServer() { + try { + ContextHandlerCollection contextHandlerCollection = new ContextHandlerCollection(); + contextHandlerCollection.setHandlers(new Handler[]{buildAPIHandler(), buildSwaggerUIOverrideHandler(), buildSwaggerUIHandler()}); + // Start server + InetSocketAddress socketAddress = new InetSocketAddress(bisqEnvironment.getHttpApiHost(), bisqEnvironment.getHttpApiPort()); + Server server = new Server(socketAddress); + server.setHandler(contextHandlerCollection); + server.setRequestLog(new Slf4jRequestLog()); + server.start(); + log.info("HTTP API started on {}", socketAddress); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/http-api/src/main/java/bisq/httpapi/service/endpoint/VersionEndpoint.java b/http-api/src/main/java/bisq/httpapi/service/endpoint/VersionEndpoint.java new file mode 100644 index 00000000000..c0c89d3c815 --- /dev/null +++ b/http-api/src/main/java/bisq/httpapi/service/endpoint/VersionEndpoint.java @@ -0,0 +1,30 @@ +package bisq.httpapi.service.endpoint; + +import bisq.common.app.Version; + + + +import bisq.httpapi.model.VersionDetails; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import javax.ws.rs.GET; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + + +@Tag(name = "version") +@Produces(MediaType.APPLICATION_JSON) +public class VersionEndpoint { + + @Operation(summary = "Get version details") + @GET + public VersionDetails getVersionDetails() { + VersionDetails versionDetails = new VersionDetails(); + versionDetails.application = Version.VERSION; + versionDetails.network = Version.P2P_NETWORK_VERSION; + versionDetails.p2PMessage = Version.getP2PMessageVersion(); + versionDetails.localDB = Version.LOCAL_DB_VERSION; + versionDetails.tradeProtocol = Version.TRADE_PROTOCOL_VERSION; + return versionDetails; + } +} diff --git a/http-api/src/main/resources/META-INF/custom-swagger-ui/index.html b/http-api/src/main/resources/META-INF/custom-swagger-ui/index.html new file mode 100644 index 00000000000..4847a0bbe4b --- /dev/null +++ b/http-api/src/main/resources/META-INF/custom-swagger-ui/index.html @@ -0,0 +1,58 @@ + + + + + + Swagger UI + + + + + + + +
+ + + + + diff --git a/http-api/src/main/resources/openapi-configuration.json b/http-api/src/main/resources/openapi-configuration.json new file mode 100644 index 00000000000..29387424917 --- /dev/null +++ b/http-api/src/main/resources/openapi-configuration.json @@ -0,0 +1,6 @@ +{ + "prettyPrint": true, + "cacheTTL": 0, + "openAPI": { + } +} diff --git a/http-api/src/testIntegration/java/bisq/httpapi/ApiTestHelper.java b/http-api/src/testIntegration/java/bisq/httpapi/ApiTestHelper.java new file mode 100644 index 00000000000..71078874222 --- /dev/null +++ b/http-api/src/testIntegration/java/bisq/httpapi/ApiTestHelper.java @@ -0,0 +1,13 @@ +package bisq.httpapi; + +@SuppressWarnings("WeakerAccess") +public final class ApiTestHelper { + + public static void waitForAllServicesToBeReady() throws InterruptedException { +// TODO it would be nice to expose endpoint that would respond with 200 + // PaymentMethod initializes it's static values after all services get initialized + int ALL_SERVICES_INITIALIZED_DELAY = 5000; + Thread.sleep(ALL_SERVICES_INITIALIZED_DELAY); + } + +} diff --git a/http-api/src/testIntegration/java/bisq/httpapi/ContainerFactory.java b/http-api/src/testIntegration/java/bisq/httpapi/ContainerFactory.java new file mode 100644 index 00000000000..c220588d7b6 --- /dev/null +++ b/http-api/src/testIntegration/java/bisq/httpapi/ContainerFactory.java @@ -0,0 +1,84 @@ +package bisq.httpapi; + +import org.arquillian.cube.docker.impl.client.config.Await; +import org.arquillian.cube.docker.impl.client.containerobject.dsl.Container; +import org.arquillian.cube.docker.impl.client.containerobject.dsl.ContainerBuilder; + +@SuppressWarnings("WeakerAccess") +public final class ContainerFactory { + + public static final String BITCOIN_NODE_CONTAINER_NAME = "bisq-http-api-bitcoin-node"; + public static final String BITCOIN_NODE_HOST_NAME = "bitcoin"; + public static final String SEED_NODE_CONTAINER_NAME = "bisq-seednode"; + public static final String SEED_NODE_HOST_NAME = SEED_NODE_CONTAINER_NAME; + public static final String SEED_NODE_ADDRESS = SEED_NODE_HOST_NAME + ":8000"; + public static final String CONTAINER_NAME_PREFIX = "bisq-http-api-"; + public static final String API_IMAGE = "bisq/http-api"; + public static final String ENV_NODE_PORT_KEY = "NODE_PORT"; + public static final String ENV_ENABLE_HTTP_API_EXPERIMENTAL_FEATURES_KEY = "ENABLE_HTTP_API_EXPERIMENTAL_FEATURES"; + public static final String ENV_HTTP_API_HOST_KEY = "HTTP_API_HOST"; + public static final String ENV_HTTP_API_HOST_VALUE = "0.0.0.0"; + public static final String ENV_USE_DEV_PRIVILEGE_KEYS_KEY = "USE_DEV_PRIVILEGE_KEYS"; + public static final String ENV_USE_DEV_PRIVILEGE_KEYS_VALUE = "true"; + public static final String ENV_USE_LOCALHOST_FOR_P2P_KEY = "USE_LOCALHOST_FOR_P2P"; + public static final String ENV_USE_LOCALHOST_FOR_P2P_VALUE = "true"; + public static final String ENV_BASE_CURRENCY_NETWORK_KEY = "BASE_CURRENCY_NETWORK"; + public static final String ENV_BASE_CURRENCY_NETWORK_VALUE = "BTC_REGTEST"; + public static final String ENV_BITCOIN_REGTEST_HOST_KEY = "BITCOIN_REGTEST_HOST"; + public static final String ENV_BITCOIN_REGTEST_HOST_VALUE = "NONE"; + public static final String ENV_BTC_NODES_KEY = "BTC_NODES"; + public static final String ENV_BTC_NODES_VALUE = "bitcoin:18444"; + public static final String ENV_SEED_NODES_KEY = "SEED_NODES"; + public static final String ENV_SEED_NODES_VALUE = SEED_NODE_ADDRESS; + public static final String ENV_LOG_LEVEL_KEY = "LOG_LEVEL"; + public static final String ENV_LOG_LEVEL_VALUE = "debug"; + + @SuppressWarnings("WeakerAccess") + public static ContainerBuilder.ContainerOptionsBuilder createApiContainerBuilder(String nameSuffix, String portBinding, int nodePort, boolean linkToSeedNode, boolean linkToBitcoin, boolean enableExperimentalFeatures) { + ContainerBuilder.ContainerOptionsBuilder containerOptionsBuilder = Container.withContainerName(CONTAINER_NAME_PREFIX + nameSuffix) + .fromImage(API_IMAGE) + .withPortBinding(portBinding) + .withEnvironment(ENV_NODE_PORT_KEY, nodePort) + .withEnvironment(ENV_HTTP_API_HOST_KEY, ENV_HTTP_API_HOST_VALUE) + .withEnvironment(ENV_ENABLE_HTTP_API_EXPERIMENTAL_FEATURES_KEY, enableExperimentalFeatures) + .withEnvironment(ENV_USE_DEV_PRIVILEGE_KEYS_KEY, ENV_USE_DEV_PRIVILEGE_KEYS_VALUE) + .withAwaitStrategy(getAwaitStrategy()); + if (linkToSeedNode) { + containerOptionsBuilder.withLink(SEED_NODE_CONTAINER_NAME); + } + if (linkToBitcoin) { + containerOptionsBuilder.withLink(BITCOIN_NODE_CONTAINER_NAME, BITCOIN_NODE_HOST_NAME); + } + return withRegtestEnv(containerOptionsBuilder); + } + + public static Await getAwaitStrategy() { + Await awaitStrategy = new Await(); + awaitStrategy.setStrategy("polling"); + int sleepPollingTime = 250; + awaitStrategy.setIterations(60000 / sleepPollingTime); + awaitStrategy.setSleepPollingTime(sleepPollingTime); + return awaitStrategy; + } + + public static Container createApiContainer(String nameSuffix, String portBinding, int nodePort, boolean linkToSeedNode, boolean linkToBitcoin, boolean enableExperimentalFeatures) { + Container container = createApiContainerBuilder(nameSuffix, portBinding, nodePort, linkToSeedNode, linkToBitcoin, enableExperimentalFeatures).build(); + container.getCubeContainer().setKillContainer(true); + return container; + } + + public static Container createApiContainer(String nameSuffix, String portBinding, int nodePort, boolean linkToSeedNode, boolean linkToBitcoin) { + return createApiContainer(nameSuffix, portBinding, nodePort, linkToSeedNode, linkToBitcoin, true); + } + + public static ContainerBuilder.ContainerOptionsBuilder withRegtestEnv(ContainerBuilder.ContainerOptionsBuilder builder) { + return builder + .withEnvironment(ENV_USE_LOCALHOST_FOR_P2P_KEY, ENV_USE_LOCALHOST_FOR_P2P_VALUE) + .withEnvironment(ENV_BASE_CURRENCY_NETWORK_KEY, ENV_BASE_CURRENCY_NETWORK_VALUE) + .withEnvironment(ENV_BITCOIN_REGTEST_HOST_KEY, ENV_BITCOIN_REGTEST_HOST_VALUE) + .withEnvironment(ENV_BTC_NODES_KEY, ENV_BTC_NODES_VALUE) + .withEnvironment(ENV_SEED_NODES_KEY, ENV_SEED_NODES_VALUE) + .withEnvironment(ENV_LOG_LEVEL_KEY, ENV_LOG_LEVEL_VALUE); + } + +} diff --git a/http-api/src/testIntegration/java/bisq/httpapi/RegexMatcher.java b/http-api/src/testIntegration/java/bisq/httpapi/RegexMatcher.java new file mode 100644 index 00000000000..2793c13f065 --- /dev/null +++ b/http-api/src/testIntegration/java/bisq/httpapi/RegexMatcher.java @@ -0,0 +1,28 @@ +package bisq.httpapi; + +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; + +public class RegexMatcher extends TypeSafeMatcher { + + private final String regex; + + private RegexMatcher(String regex) { + this.regex = regex; + } + + @Override + public void describeTo(Description description) { + description.appendText("matches regex=`" + regex + "`"); + } + + @Override + public boolean matchesSafely(String string) { + return string.matches(regex); + } + + @SuppressWarnings("WeakerAccess") + public static RegexMatcher matchesRegex(String regex) { + return new RegexMatcher(regex); + } +} diff --git a/http-api/src/testIntegration/java/bisq/httpapi/SwaggerIT.java b/http-api/src/testIntegration/java/bisq/httpapi/SwaggerIT.java new file mode 100644 index 00000000000..48fde654a0f --- /dev/null +++ b/http-api/src/testIntegration/java/bisq/httpapi/SwaggerIT.java @@ -0,0 +1,61 @@ +package bisq.httpapi; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; + + + +import org.arquillian.cube.docker.impl.client.containerobject.dsl.Container; +import org.arquillian.cube.docker.impl.client.containerobject.dsl.DockerContainer; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.junit.InSequence; + +@RunWith(Arquillian.class) +public class SwaggerIT { + + @DockerContainer + private Container alice = ContainerFactory.createApiContainer("alice", "8081->8080", 3333, false, false); + + @InSequence(1) + @Test + public void getDocs_always_returns200() { + int alicePort = getAlicePort(); + + given(). + port(alicePort). +// + when(). + get("/docs"). +// + then(). + statusCode(200). + and().body(containsString("Swagger UI")) + ; + } + + @InSequence(1) + @Test + public void getOpenApiJson_always_returns200() { + int alicePort = getAlicePort(); + + given(). + port(alicePort). +// + when(). + get("/openapi.json"). +// + then(). + statusCode(200). + and().body("info.title", equalTo("Bisq HTTP API")) + ; + } + + private int getAlicePort() { + return alice.getBindPort(8080); + } + +} diff --git a/http-api/src/testIntegration/java/bisq/httpapi/VersionEndpointIT.java b/http-api/src/testIntegration/java/bisq/httpapi/VersionEndpointIT.java new file mode 100644 index 00000000000..9945008d19a --- /dev/null +++ b/http-api/src/testIntegration/java/bisq/httpapi/VersionEndpointIT.java @@ -0,0 +1,56 @@ +package bisq.httpapi; + +import bisq.common.app.Version; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.isA; + + + +import org.arquillian.cube.docker.impl.client.containerobject.dsl.Container; +import org.arquillian.cube.docker.impl.client.containerobject.dsl.DockerContainer; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.junit.InSequence; + +@RunWith(Arquillian.class) +public class VersionEndpointIT { + + @DockerContainer + private Container alice = ContainerFactory.createApiContainer("alice", "8081->8080", 3333, false, false); + + @InSequence + @Test + public void waitForAllServicesToBeReady() throws InterruptedException { + ApiTestHelper.waitForAllServicesToBeReady(); + } + + @InSequence(1) + @Test + public void getVersionDetails_always_returns200() { + int alicePort = getAlicePort(); + + given(). + port(alicePort). +// + when(). + get("/api/v1/version"). +// + then(). + statusCode(200). + and().body("application", equalTo(Version.VERSION)). + and().body("network", equalTo(Version.P2P_NETWORK_VERSION)). + and().body("p2PMessage", isA(Integer.class)). + and().body("localDB", equalTo(Version.LOCAL_DB_VERSION)). + and().body("tradeProtocol", equalTo(Version.TRADE_PROTOCOL_VERSION)) + ; + } + + private int getAlicePort() { + return alice.getBindPort(8080); + } + +} diff --git a/http-api/src/testIntegration/java/bisq/httpapi/arquillian/CubeLogger.java b/http-api/src/testIntegration/java/bisq/httpapi/arquillian/CubeLogger.java new file mode 100644 index 00000000000..061e1935bc4 --- /dev/null +++ b/http-api/src/testIntegration/java/bisq/httpapi/arquillian/CubeLogger.java @@ -0,0 +1,30 @@ +package bisq.httpapi.arquillian; + +import org.arquillian.cube.CubeController; +import org.arquillian.cube.spi.event.lifecycle.BeforeStop; +import org.jboss.arquillian.config.descriptor.api.ArquillianDescriptor; +import org.jboss.arquillian.core.api.annotation.Observes; +import org.jboss.arquillian.test.spi.TestClass; + +public class CubeLogger { + + @SuppressWarnings({"unused", "UnusedParameters"}) + public void beforeContainerStop(@Observes BeforeStop event, CubeController cubeController, ArquillianDescriptor arquillianDescriptor, TestClass testClass) { + if (isExtensionEnabled(arquillianDescriptor)) { + String cubeId = event.getCubeId(); + System.out.println("====================================================================================="); + System.out.println("Start of container logs: " + cubeId + " from " + testClass.getName()); + System.out.println("====================================================================================="); + cubeController.copyLog(cubeId, false, true, true, true, -1, System.out); + System.out.println("====================================================================================="); + System.out.println("End of container logs: " + cubeId + " from " + testClass.getName()); + System.out.println("====================================================================================="); + } + } + + private static boolean isExtensionEnabled(ArquillianDescriptor arquillianDescriptor) { + String dumpContainerLogs = arquillianDescriptor.extension("cubeLogger").getExtensionProperty("enable"); + return Boolean.parseBoolean(dumpContainerLogs); + } + +} diff --git a/http-api/src/testIntegration/java/bisq/httpapi/arquillian/CubeLoggerExtension.java b/http-api/src/testIntegration/java/bisq/httpapi/arquillian/CubeLoggerExtension.java new file mode 100644 index 00000000000..de1a9da630b --- /dev/null +++ b/http-api/src/testIntegration/java/bisq/httpapi/arquillian/CubeLoggerExtension.java @@ -0,0 +1,11 @@ +package bisq.httpapi.arquillian; + +import org.jboss.arquillian.core.spi.LoadableExtension; + +public class CubeLoggerExtension implements LoadableExtension { + + @Override + public void register(ExtensionBuilder extensionBuilder) { + extensionBuilder.observer(CubeLogger.class); + } +} diff --git a/http-api/src/testIntegration/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension b/http-api/src/testIntegration/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension new file mode 100644 index 00000000000..56be38f6cd0 --- /dev/null +++ b/http-api/src/testIntegration/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension @@ -0,0 +1 @@ +bisq.httpapi.arquillian.CubeLoggerExtension diff --git a/http-api/src/testIntegration/resources/arquillian.xml b/http-api/src/testIntegration/resources/arquillian.xml new file mode 100644 index 00000000000..45de142b060 --- /dev/null +++ b/http-api/src/testIntegration/resources/arquillian.xml @@ -0,0 +1,13 @@ + + + + + CUBE + unix:///var/run/docker.sock + false + + + true + + diff --git a/http-api/src/testIntegration/resources/logback-test.xml b/http-api/src/testIntegration/resources/logback-test.xml new file mode 100644 index 00000000000..5ebd64fe58e --- /dev/null +++ b/http-api/src/testIntegration/resources/logback-test.xml @@ -0,0 +1,15 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + diff --git a/settings.gradle b/settings.gradle index 500b2803c47..e56e7aeb152 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,6 +2,7 @@ include 'assets' include 'common' include 'p2p' include 'core' +include 'http-api' include 'desktop' include 'monitor' include 'pricenode'