diff --git a/browserup-proxy-rest-clients/build.gradle b/browserup-proxy-rest-clients/build.gradle new file mode 100644 index 000000000..eaf95fbce --- /dev/null +++ b/browserup-proxy-rest-clients/build.gradle @@ -0,0 +1,152 @@ +/* + * Modifications Copyright (c) 2019 BrowserUp, Inc. + */ + +plugins { + id 'java-library' + id 'groovy' + id "io.swagger.core.v3.swagger-gradle-plugin" version "2.1.6" + id 'org.openapi.generator' version '4.3.1' +} + +ext { + jerseyVersion = '2.32' +} + +resolve { + outputFileName = 'openapi' + outputFormat = 'YAML' + prettyPrint = 'TRUE' + openApiFile = file("src/main/resources/openapi-config.json") + classpath = sourceSets.main.runtimeClasspath + readerClass = "com.browserup.bup.rest.openapi.CustomOpenApiReader" + resourcePackages = ['com.browserup.bup.rest.resource'] + outputPath = "$buildDir/openapi" +} + +class Constants { + static String apiPackage = 'browserup' + static String modelPackage = 'browserup.model' +} + +/* + https://github.com/OpenAPITools/openapi-generator/issues/3285 + */ +class PythonClientPostProcessor { + String projectDir + + void process() { + def clientDir = new File("$projectDir/build/openapi-clients/python") + clientDir.eachFileRecurse { + if (it.name.endsWith(".py")) { + processInitFile(it) + } + } + new File("${clientDir}/openapi_client/${Constants.apiPackage}/model/__init__.py") << + new File("${clientDir}/openapi_client/${Constants.modelPackage}/__init__.py").text + } + + private static void processInitFile(File initFile) { + initFile.text = initFile.text.replaceAll( + ~/(from ${Constants.apiPackage}.default_api import)/, + "from openapi_client.${Constants.apiPackage}.default_api import" + ) + } +} + +class ClientInfo { + String language + Closure postProcessor +} + +def clients = [ + new ClientInfo(language: 'JavaScript'), + new ClientInfo(language: 'Ruby'), + new ClientInfo( + language: 'Python', + postProcessor: new PythonClientPostProcessor(projectDir: projectDir).&process) +] as ClientInfo[] + +clients.each { client -> + def lang = client.language + def postProcessor = client.postProcessor + + task "openApiGenerate${lang}Client"(type: org.openapitools.generator.gradle.plugin.tasks.GenerateTask) { + def language = lang.toLowerCase() + generatorName = language + inputSpec = "$buildDir/openapi/openapi.yaml".toString() + outputDir = "$buildDir/openapi-clients/$language/".toString() + apiPackage = Constants.apiPackage + modelPackage = Constants.modelPackage + invokerPackage = "browserup.invoker" + systemProperties = [ + modelDocs: 'false' + ] + skipValidateSpec = true + logToStderr = true + generateAliasAsModel = false + } + if (postProcessor) tasks.getByName("openApiGenerate${lang}Client").doLast(postProcessor) +} + +test { + testLogging.showStandardStreams = true +} + +task openApiGenerateClients(dependsOn: resolve) { + clients.each { c -> + dependsOn "openApiGenerate${c.language}Client" + } + + doLast { + clients.each { client -> + def langName = client.language.toLowerCase() + delete "src/test/${langName}/client" + copy { + from "$buildDir/openapi-clients/${langName}/" + into "src/test/${langName}/client" + } + } + + project.delete "$buildDir/openapi-clients" + } +} + +archivesBaseName = 'browserup-proxy-rest-clients' + +dependencies { + implementation project(':browserup-proxy-core') + implementation project(':browserup-proxy-rest') + + testImplementation "org.glassfish.jersey.containers:jersey-container-servlet-core:${jerseyVersion}" + testImplementation "org.glassfish.jersey.media:jersey-media-json-jackson:${jerseyVersion}" + testImplementation "org.glassfish.jersey.inject:jersey-hk2:${jerseyVersion}" + testImplementation "org.glassfish.jersey.ext:jersey-bean-validation:${jerseyVersion}" + + testImplementation project(':browserup-proxy-mitm') + + testImplementation "com.google.inject:guice:$guiceVersion" + testImplementation "com.google.inject.extensions:guice-servlet:$guiceVersion" + testImplementation "com.google.inject.extensions:guice-multibindings:$guiceVersion" + + testImplementation 'com.google.sitebricks:sitebricks:0.8.11' + + testImplementation 'junit:junit:4.12' + testImplementation "org.apache.logging.log4j:log4j-api:${log4jVersion}" + testImplementation "org.apache.logging.log4j:log4j-core:${log4jVersion}" + testImplementation "org.apache.logging.log4j:log4j-slf4j-impl:${log4jVersion}" + testImplementation 'org.codehaus.groovy:groovy-all:2.5.7' + testImplementation 'org.codehaus.groovy.modules.http-builder:http-builder:0.7.2' + testImplementation 'org.hamcrest:hamcrest:2.1' + testImplementation 'org.hamcrest:hamcrest-library:2.1' + testImplementation 'org.mockito:mockito-core:3.0.0' + testImplementation 'org.seleniumhq.selenium:selenium-api:3.4.0' + testImplementation 'org.awaitility:awaitility:3.1.6' + testImplementation 'xyz.rogfam:littleproxy:2.0.0-beta-3' + testImplementation 'com.github.tomakehurst:wiremock-jre8:2.24.0' + testImplementation 'org.testcontainers:testcontainers:1.12.0' +} + +openApiGenerateClients.mustRunAfter(resolve) + +test.dependsOn(openApiGenerateClients) diff --git a/browserup-proxy-rest-clients/src/main/resources/openapi-config.json b/browserup-proxy-rest-clients/src/main/resources/openapi-config.json new file mode 100644 index 000000000..40dad6bc1 --- /dev/null +++ b/browserup-proxy-rest-clients/src/main/resources/openapi-config.json @@ -0,0 +1,18 @@ +{ + "resourcePackages" : [ + "com.browserup.bup.rest.resource" + ], + "openapi" : "3.0.1", + "info": { + "version": "1.0", + "title": "BrowserUp Proxy API", + "description": "BrowserUp Proxy API", + "contact": { + "email": "hello@browserup.com" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + } + } +} \ No newline at end of file diff --git a/browserup-proxy-rest-clients/src/test/groovy/com/browserup/bup/WithRunningProxyRestTest.groovy b/browserup-proxy-rest-clients/src/test/groovy/com/browserup/bup/WithRunningProxyRestTest.groovy new file mode 100644 index 000000000..997fb1a3f --- /dev/null +++ b/browserup-proxy-rest-clients/src/test/groovy/com/browserup/bup/WithRunningProxyRestTest.groovy @@ -0,0 +1,192 @@ +/* + * Modifications Copyright (c) 2019 BrowserUp, Inc. + */ + +package com.browserup.bup + +/* + * Modifications Copyright (c) 2019 BrowserUp, Inc. + */ + +import com.browserup.bup.MitmProxyServer +import com.browserup.bup.proxy.MitmProxyManager +import com.browserup.bup.proxy.bricks.ProxyResource +import com.browserup.bup.proxy.guice.ConfigModule +import com.browserup.bup.proxy.guice.JettyModule +import com.browserup.bup.util.BrowserUpProxyUtil +import com.github.tomakehurst.wiremock.junit.WireMockRule +import com.google.inject.Guice +import com.google.inject.Injector +import com.google.inject.servlet.GuiceServletContextListener +import com.google.sitebricks.SitebricksModule +import groovyx.net.http.HTTPBuilder +import groovyx.net.http.Method +import org.apache.http.entity.ContentType +import org.awaitility.Awaitility +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.servlet.ServletContextHandler +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import javax.servlet.ServletContextEvent +import java.util.concurrent.TimeUnit + +import static com.github.tomakehurst.wiremock.client.WireMock.* +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options +import static org.junit.Assert.assertEquals + +abstract class WithRunningProxyRestTest { + private static final Logger LOG = LoggerFactory.getLogger(MitmProxyManager) + + protected MitmProxyManager proxyManager + protected MitmProxyServer proxy + protected Server restServer + + protected String[] getArgs() { + ['--port', '0'] as String[] + } + + abstract String getUrlPath(); + + String getFullUrlPath() { + return "/proxy/${proxy.port}/${urlPath}" + } + + protected int mockServerPort + protected int mockServerHttpsPort + + @Rule + public WireMockRule wireMockRule = new WireMockRule(options().port(0).httpsPort(0)) + + @Before + void setUp() throws Exception { + Injector injector = Guice.createInjector(new ConfigModule(args), new JettyModule(), new SitebricksModule() { + @Override + protected void configureSitebricks() { + scan(ProxyResource.class.getPackage()) + } + }) + + proxyManager = injector.getInstance(MitmProxyManager) + + LOG.debug("Starting BrowserUp Proxy version " + BrowserUpProxyUtil.versionString) + + new Thread(new Runnable() { + @Override + void run() { + startRestServer(injector) + } + }).start() + + LOG.debug("Waiting till BrowserUp Rest server is started") + + Awaitility.await().atMost(10, TimeUnit.SECONDS).until({ -> restServer != null && restServer.isStarted() }) + + LOG.debug("BrowserUp Rest server is started successfully") + + LOG.debug("Waiting till BrowserUp Proxy server is started") + + proxy = proxyManager.create(0) + + Awaitility.await().atMost(5, TimeUnit.SECONDS).until({ -> proxyManager.get().size() > 0 }) + + LOG.debug("BrowserUp Proxy server is started successfully") + + mockServerPort = wireMockRule.port(); + mockServerHttpsPort = wireMockRule.httpsPort(); + + waitForProxyServer() + } + + def waitForProxyServer() { + Awaitility.await().atMost(5, TimeUnit.SECONDS).until({ -> + def successful = false + proxyRestServerClient.request(Method.GET, ContentType.TEXT_PLAIN) { req -> + uri.path = "/proxy" + response.success = { _, reader -> + successful = true + } + response.failure = { _, reader -> + successful = false + } + } + return successful + }) + } + + HTTPBuilder getTargetServerClient() { + def http = new HTTPBuilder("http://localhost:${mockServerPort}") + http.setProxy('localhost', proxy.port, 'http') + http + } + + HTTPBuilder getProxyRestServerClient() { + new HTTPBuilder("http://localhost:${restServer.connectors[0].localPort}") + } + + def sendGetToProxyServer(Closure configClosure) { + proxyRestServerClient.request(Method.GET, ContentType.WILDCARD, configClosure) + } + + void requestToTargetServer(url, expectedResponse) { + targetServerClient.request(Method.GET, ContentType.TEXT_PLAIN) { req -> + uri.path = "/${url}" + response.success = { _, reader -> + assertEquals(expectedResponse, reader.text) + } + response.failure = { _, reader -> + assertEquals(expectedResponse, reader.text) + } + } + } + + @After + void tearDown() throws Exception { + LOG.debug('Stopping proxy servers') + for (def proxyServer : proxyManager.get()) { + try { + proxyManager.delete(proxyServer.port) + } catch (Exception ex) { + LOG.error('Error while stopping proxy servers', ex) + } + } + + if (restServer != null) { + LOG.debug('Stopping rest proxy server') + try { + restServer.stop() + } catch (Exception ex) { + LOG.error('Error while stopping rest proxy server', ex) + } + } + } + + private void startRestServer(Injector injector) { + restServer = injector.getInstance(Server.class) + def contextListener = new GuiceServletContextListener() { + @Override + protected Injector getInjector() { + return injector + } + } + restServer.start() + contextListener.contextInitialized( + new ServletContextEvent((restServer.handler as ServletContextHandler).servletContext)) + try { + restServer.join() + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt() + } + } + + protected void mockTargetServerResponse(String url, String responseBody, int delayMilliseconds=0) { + def response = aResponse().withStatus(200) + .withBody(responseBody) + .withHeader('Content-Type', 'text/plain') + .withFixedDelay(delayMilliseconds) + stubFor(get(urlEqualTo("/${url}")).willReturn(response)) + } +} diff --git a/browserup-proxy-rest-clients/src/test/groovy/com/browserup/bup/javascript/JavaScriptClientTest.groovy b/browserup-proxy-rest-clients/src/test/groovy/com/browserup/bup/javascript/JavaScriptClientTest.groovy new file mode 100644 index 000000000..7f11824ce --- /dev/null +++ b/browserup-proxy-rest-clients/src/test/groovy/com/browserup/bup/javascript/JavaScriptClientTest.groovy @@ -0,0 +1,109 @@ +/* + * Modifications Copyright (c) 2019 BrowserUp, Inc. + */ + +package com.browserup.bup.javascript + +import com.browserup.bup.WithRunningProxyRestTest +import org.awaitility.Awaitility +import org.junit.After +import org.junit.Assert +import org.junit.Test +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.testcontainers.Testcontainers +import org.testcontainers.containers.GenericContainer +import org.testcontainers.images.builder.ImageFromDockerfile + +import java.nio.file.Path +import java.nio.file.Paths +import java.util.concurrent.TimeUnit + +class JavaScriptClientTest extends WithRunningProxyRestTest { + private static final Logger LOG = LoggerFactory.getLogger(JavaScriptClientTest) + + private GenericContainer container + + @Override + String getUrlPath() { + return 'har/entries' + } + + @After + void shutDown() { + if (container != null) { + container.stop() + } + } + + @Test + void connectToProxy() { + def urlToCatch = 'test' + def urlNotToCatch = 'missing' + def responseBody = 'success' + + mockTargetServerResponse(urlToCatch, responseBody) + mockTargetServerResponse(urlNotToCatch, responseBody) + + proxyManager.get()[0].newHar() + + requestToTargetServer(urlToCatch, responseBody) + requestToTargetServer(urlNotToCatch, responseBody) + + Testcontainers.exposeHostPorts(restServer.connectors[0].localPort as Integer) + Testcontainers.exposeHostPorts(proxy.port as Integer) + + new File('./src/test/javascript/client/node_modules').deleteDir() + + def dockerfile = new File('./src/test/javascript/Dockerfile') + container = new GenericContainer( + new ImageFromDockerfile() + .withDockerfile(Paths.get(dockerfile.path))) + .withEnv('PROXY_REST_HOST', 'host.testcontainers.internal') + .withEnv('PROXY_REST_PORT', restServer.connectors[0].localPort as String) + .withEnv('PROXY_PORT', proxy.port as String) + + container.start() + + Awaitility.await().atMost(10, TimeUnit.SECONDS).until({-> !container.isRunning()}) + + LOG.info('Docker log: ' + container.getLogs()) + + Assert.assertEquals("Expected javascript-client container exit code to be 0", 0, container.getCurrentContainerInfo().getState().getExitCode()) + } + + @Test + void failsToConnectToProxy() { + def urlToCatch = 'test' + def urlNotToCatch = 'missing' + def responseBody = 'success' + def invalidProxyPort = 8 + + mockTargetServerResponse(urlToCatch, responseBody) + mockTargetServerResponse(urlNotToCatch, responseBody) + + proxyManager.get()[0].newHar() + + requestToTargetServer(urlToCatch, responseBody) + requestToTargetServer(urlNotToCatch, responseBody) + + Testcontainers.exposeHostPorts(restServer.connectors[0].localPort as Integer) + Testcontainers.exposeHostPorts(proxy.port as Integer) + + def dockerfile = new File('./src/test/javascript/Dockerfile') + container = new GenericContainer( + new ImageFromDockerfile() + .withDockerfile(Paths.get(dockerfile.path))) + .withEnv('PROXY_REST_HOST', 'host.testcontainers.internal') + .withEnv('PROXY_REST_PORT', restServer.connectors[0].localPort as String) + .withEnv('PROXY_PORT', invalidProxyPort as String) + + container.start() + + Awaitility.await().atMost(10, TimeUnit.SECONDS).until({-> !container.isRunning()}) + + LOG.info('Docker log: ' + container.getLogs()) + + Assert.assertEquals("Expected javascript-client container exit code to be 1", 1, container.getCurrentContainerInfo().getState().getExitCode()) + } +} diff --git a/browserup-proxy-rest-clients/src/test/groovy/com/browserup/bup/python/PythonTestClient.groovy b/browserup-proxy-rest-clients/src/test/groovy/com/browserup/bup/python/PythonTestClient.groovy new file mode 100644 index 000000000..d1c8b5da8 --- /dev/null +++ b/browserup-proxy-rest-clients/src/test/groovy/com/browserup/bup/python/PythonTestClient.groovy @@ -0,0 +1,109 @@ +/* + * Modifications Copyright (c) 2019 BrowserUp, Inc. + */ + +package com.browserup.bup.python + +import com.browserup.bup.WithRunningProxyRestTest +import org.awaitility.Awaitility +import org.junit.After +import org.junit.Assert +import org.junit.Ignore +import org.junit.Test +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.testcontainers.Testcontainers +import org.testcontainers.containers.GenericContainer +import org.testcontainers.images.builder.ImageFromDockerfile + +import java.nio.file.Path +import java.nio.file.Paths +import java.util.concurrent.TimeUnit + +class PythonTestClient extends WithRunningProxyRestTest { + private static final Logger LOG = LoggerFactory.getLogger(PythonTestClient) + + private GenericContainer container + + @Override + String getUrlPath() { + return 'har/entries' + } + + @After + void shutDown() { + if (container != null) { + container.stop() + } + } + + @Test + void connectToProxySuccessfully() { + def urlToCatch = 'test' + def urlNotToCatch = 'missing' + def responseBody = 'success' + + mockTargetServerResponse(urlToCatch, responseBody) + mockTargetServerResponse(urlNotToCatch, responseBody) + + proxyManager.get()[0].newHar() + + requestToTargetServer(urlToCatch, responseBody) + requestToTargetServer(urlNotToCatch, responseBody) + + Testcontainers.exposeHostPorts(restServer.connectors[0].localPort as Integer) + Testcontainers.exposeHostPorts(proxy.port as Integer) + + def dockerfile = new File('./src/test/python/Dockerfile') + container = new GenericContainer( + new ImageFromDockerfile() + .withDockerfile(Paths.get(dockerfile.path))) + .withEnv('PROXY_REST_HOST', 'host.testcontainers.internal') + .withEnv('PROXY_REST_PORT', restServer.connectors[0].localPort as String) + .withEnv('PROXY_PORT', proxy.port as String) + + container.start() + + Awaitility.await().atMost(10, TimeUnit.SECONDS).until({-> !container.isRunning()}) + + LOG.info('Docker log: ' + container.getLogs()) + + Assert.assertEquals("Expected python-client container exit code to be 0", 0, container.getCurrentContainerInfo().getState().getExitCode()) + } + + @Test + void failsToConnectToProxy() { + def urlToCatch = 'test' + def urlNotToCatch = 'missing' + def responseBody = 'success' + def invalidProxyPort = 8 + + mockTargetServerResponse(urlToCatch, responseBody) + mockTargetServerResponse(urlNotToCatch, responseBody) + + proxyManager.get()[0].newHar() + + requestToTargetServer(urlToCatch, responseBody) + requestToTargetServer(urlNotToCatch, responseBody) + + Testcontainers.exposeHostPorts(restServer.connectors[0].localPort as Integer) + Testcontainers.exposeHostPorts(proxy.port as Integer) + + def dockerfile = new File('./src/test/python/Dockerfile') + container = new GenericContainer( + new ImageFromDockerfile() + .withDockerfile(Paths.get(dockerfile.path))) + .withEnv('PROXY_REST_HOST', 'host.testcontainers.internal') + .withEnv('PROXY_REST_PORT', restServer.connectors[0].localPort as String) + .withEnv('PROXY_PORT', invalidProxyPort as String) + + container.start() + + Awaitility.await().atMost(10, TimeUnit.SECONDS).until({-> !container.isRunning()}) + + LOG.info('Docker log: ' + container.getLogs()) + + Assert.assertEquals("Expected python-client container exit code to be 1", 1, container.getCurrentContainerInfo().getState().getExitCode()) + } +} + diff --git a/browserup-proxy-rest-clients/src/test/groovy/com/browserup/bup/ruby/RubyClientTest.groovy b/browserup-proxy-rest-clients/src/test/groovy/com/browserup/bup/ruby/RubyClientTest.groovy new file mode 100644 index 000000000..90ae48276 --- /dev/null +++ b/browserup-proxy-rest-clients/src/test/groovy/com/browserup/bup/ruby/RubyClientTest.groovy @@ -0,0 +1,108 @@ +/* + * Modifications Copyright (c) 2019 BrowserUp, Inc. + */ + +package com.browserup.bup.ruby + +import com.browserup.bup.WithRunningProxyRestTest +import org.awaitility.Awaitility +import org.junit.After +import org.junit.Assert +import org.junit.Test +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.testcontainers.Testcontainers +import org.testcontainers.containers.Container +import org.testcontainers.containers.GenericContainer +import org.testcontainers.images.builder.ImageFromDockerfile + +import java.nio.file.Path +import java.nio.file.Paths +import java.util.concurrent.TimeUnit + +class RubyClientTest extends WithRunningProxyRestTest { + private static final Logger LOG = LoggerFactory.getLogger(RubyClientTest) + + private GenericContainer container + + @Override + String getUrlPath() { + return 'har/entries' + } + + @After + void shutDown() { + if (container != null) { + container.stop() + } + } + + @Test + void connectToProxy() { + def urlToCatch = 'test' + def urlNotToCatch = 'missing' + def responseBody = 'success' + + mockTargetServerResponse(urlToCatch, responseBody) + mockTargetServerResponse(urlNotToCatch, responseBody) + + proxyManager.get()[0].newHar() + + requestToTargetServer(urlToCatch, responseBody) + requestToTargetServer(urlNotToCatch, responseBody) + + Testcontainers.exposeHostPorts(restServer.connectors[0].localPort as Integer) + Testcontainers.exposeHostPorts(proxy.port as Integer) + + def dockerfile = new File('./src/test/ruby/Dockerfile') + container = new GenericContainer( + new ImageFromDockerfile() + .withDockerfile(Paths.get(dockerfile.path))) + .withEnv('PROXY_REST_HOST', 'host.testcontainers.internal') + .withEnv('PROXY_REST_PORT', restServer.connectors[0].localPort as String) + .withEnv('PROXY_PORT', proxy.port as String) + + container.start() + + Awaitility.await().atMost(10, TimeUnit.SECONDS).until({-> !container.isRunning()}) + + LOG.info('Docker log: ' + container.getLogs()) + + Assert.assertEquals("Expected ruby-client container exit code to be 0", 0, container.getCurrentContainerInfo().getState().getExitCode()) + } + + @Test + void failsToConnectToProxy() { + def urlToCatch = 'test' + def urlNotToCatch = 'missing' + def responseBody = 'success' + def invalidProxyPort = 8 + + mockTargetServerResponse(urlToCatch, responseBody) + mockTargetServerResponse(urlNotToCatch, responseBody) + + proxyManager.get()[0].newHar() + + requestToTargetServer(urlToCatch, responseBody) + requestToTargetServer(urlNotToCatch, responseBody) + + Testcontainers.exposeHostPorts(restServer.connectors[0].localPort as Integer) + Testcontainers.exposeHostPorts(proxy.port as Integer) + + def dockerfile = new File('./src/test/ruby/Dockerfile') + container = new GenericContainer( + new ImageFromDockerfile() + .withDockerfile(Paths.get(dockerfile.path))) + .withEnv('PROXY_REST_HOST', 'host.testcontainers.internal') + .withEnv('PROXY_REST_PORT', restServer.connectors[0].localPort as String) + .withEnv('PROXY_PORT', invalidProxyPort as String) + + container.start() + + Awaitility.await().atMost(10, TimeUnit.SECONDS).until({-> !container.isRunning()}) + + LOG.info('Docker log: ' + container.getLogs()) + + Assert.assertEquals("Expected ruby-client container exit code to be 1", 1, container.getCurrentContainerInfo().getState().getExitCode()) + } +} diff --git a/browserup-proxy-rest-clients/src/test/javascript/.gitignore b/browserup-proxy-rest-clients/src/test/javascript/.gitignore new file mode 100644 index 000000000..7f269dec1 --- /dev/null +++ b/browserup-proxy-rest-clients/src/test/javascript/.gitignore @@ -0,0 +1 @@ +client/ \ No newline at end of file diff --git a/browserup-proxy-rest-clients/src/test/javascript/Dockerfile b/browserup-proxy-rest-clients/src/test/javascript/Dockerfile new file mode 100644 index 000000000..a60a240eb --- /dev/null +++ b/browserup-proxy-rest-clients/src/test/javascript/Dockerfile @@ -0,0 +1,20 @@ +FROM node:8.16.0-alpine + +USER root + +WORKDIR / + +COPY ./client/ /client/ + +# Build javascript client, install locally +WORKDIR /client/ +RUN rm -rf node_modules/ +RUN npm install +RUN npm link +RUN npm link /client +RUN npm run build + +COPY . /javascript/ +WORKDIR /javascript/ + +CMD ["node", "test/javascript_test.js"] \ No newline at end of file diff --git a/browserup-proxy-rest-clients/src/test/javascript/test/javascript_test.js b/browserup-proxy-rest-clients/src/test/javascript/test/javascript_test.js new file mode 100644 index 000000000..d5fdd7ea8 --- /dev/null +++ b/browserup-proxy-rest-clients/src/test/javascript/test/javascript_test.js @@ -0,0 +1,16 @@ +var BrowserUpProxyApi = require('/client/node_modules/browser_up_proxy_api'); + + +var api = new BrowserUpProxyApi.DefaultApi() +api.apiClient.basePath = 'http://' + process.env.PROXY_REST_HOST + ':' + process.env.PROXY_REST_PORT +var port = process.env.PROXY_PORT; +var urlPattern = ".*"; +var callback = function(error, data, response) { + if (error) { + console.error(error); + throw new Error(error); + } else { + console.log('API called successfully. Returned data: ' + data); + } +}; +api.entries(port, urlPattern, callback); \ No newline at end of file diff --git a/browserup-proxy-rest-clients/src/test/python/.gitignore b/browserup-proxy-rest-clients/src/test/python/.gitignore new file mode 100644 index 000000000..7f269dec1 --- /dev/null +++ b/browserup-proxy-rest-clients/src/test/python/.gitignore @@ -0,0 +1 @@ +client/ \ No newline at end of file diff --git a/browserup-proxy-rest-clients/src/test/python/Dockerfile b/browserup-proxy-rest-clients/src/test/python/Dockerfile new file mode 100644 index 000000000..cdf081547 --- /dev/null +++ b/browserup-proxy-rest-clients/src/test/python/Dockerfile @@ -0,0 +1,16 @@ +FROM python:2.7-alpine + +WORKDIR / + +COPY client/ /python-client/ + +# Build python client, install locally +WORKDIR /python-client/ +RUN python setup.py install --user +RUN pip install -r requirements.txt +RUN pip install -r test-requirements.txt + +COPY . /python/ +WORKDIR /python/ + +CMD ["python", "test/python_test.py"] \ No newline at end of file diff --git a/browserup-proxy-rest-clients/src/test/python/test/python_test.py b/browserup-proxy-rest-clients/src/test/python/test/python_test.py new file mode 100644 index 000000000..8d7b5e9c5 --- /dev/null +++ b/browserup-proxy-rest-clients/src/test/python/test/python_test.py @@ -0,0 +1,22 @@ +from __future__ import print_function +import time +import openapi_client +import os + +from openapi_client.rest import ApiException +from pprint import pprint + +# Create an instance of the API class +api_client = openapi_client.ApiClient() +api_client.configuration.host = 'http://' + os.environ['PROXY_REST_HOST'] + ':' + os.environ['PROXY_REST_PORT'] + +api_instance = openapi_client.DefaultApi(api_client) +port = os.environ['PROXY_PORT'] +url_pattern = '.*' + +try: + api_response = api_instance.entries(port, url_pattern) + pprint(api_response) +except ApiException as e: + print("Exception when calling DefaultApi->entries: %s\n" % e) + raise \ No newline at end of file diff --git a/browserup-proxy-rest-clients/src/test/resources/log4j2-test.json b/browserup-proxy-rest-clients/src/test/resources/log4j2-test.json new file mode 100644 index 000000000..616ff8763 --- /dev/null +++ b/browserup-proxy-rest-clients/src/test/resources/log4j2-test.json @@ -0,0 +1,41 @@ +{ + "configuration" : { + "name": "test", + "appenders": { + "Console": { + "name": "console", + "target": "SYSTEM_OUT", + "PatternLayout": { + "pattern": "%-7r %date %level [%thread] %logger - %msg%n" + } + } + }, + + "loggers": { + "logger": [ + { + "name": "org.testcontainers", + "level": "warn", + "additivity": false, + "AppenderRef": { + "ref": "console" + } + }, + { + "name": "com.github.dockerjava", + "level": "warn", + "additivity": false, + "AppenderRef": { + "ref": "console" + } + } + ], + "root": { + "level": "info", + "appender-ref": { + "ref": "console" + } + } + } + } +} \ No newline at end of file diff --git a/browserup-proxy-rest-clients/src/test/ruby/.gitignore b/browserup-proxy-rest-clients/src/test/ruby/.gitignore new file mode 100644 index 000000000..7f269dec1 --- /dev/null +++ b/browserup-proxy-rest-clients/src/test/ruby/.gitignore @@ -0,0 +1 @@ +client/ \ No newline at end of file diff --git a/browserup-proxy-rest-clients/src/test/ruby/.rspec b/browserup-proxy-rest-clients/src/test/ruby/.rspec new file mode 100644 index 000000000..5052887a0 --- /dev/null +++ b/browserup-proxy-rest-clients/src/test/ruby/.rspec @@ -0,0 +1 @@ +--color \ No newline at end of file diff --git a/browserup-proxy-rest-clients/src/test/ruby/Dockerfile b/browserup-proxy-rest-clients/src/test/ruby/Dockerfile new file mode 100644 index 000000000..20b1e543f --- /dev/null +++ b/browserup-proxy-rest-clients/src/test/ruby/Dockerfile @@ -0,0 +1,20 @@ +FROM ruby:2.5 + +RUN bundle config --global frozen 1 + +WORKDIR / + +COPY ./client/ /ruby-client/ + +# Build ruby client gem, install locally +WORKDIR /ruby-client/ +RUN gem build openapi_client.gemspec +RUN gem install ./openapi_client-1.0.0.gem + +COPY . /ruby/ +WORKDIR /ruby/ + +RUN bundle config --delete frozen +RUN bundle install + +CMD ["bundle", "exec", "rspec", "--backtrace"] \ No newline at end of file diff --git a/browserup-proxy-rest-clients/src/test/ruby/Gemfile b/browserup-proxy-rest-clients/src/test/ruby/Gemfile new file mode 100644 index 000000000..6ec050d94 --- /dev/null +++ b/browserup-proxy-rest-clients/src/test/ruby/Gemfile @@ -0,0 +1,9 @@ +source 'https://rubygems.org' + +group :development, :test do + gem 'rake', '~> 12.0.0' + gem 'pry-byebug' + gem 'rspec' + gem 'rubocop', '~> 0.66.0' + gem 'openapi_client', '~> 1.0.0' +end diff --git a/browserup-proxy-rest-clients/src/test/ruby/Gemfile.lock b/browserup-proxy-rest-clients/src/test/ruby/Gemfile.lock new file mode 100644 index 000000000..2ac5a88b3 --- /dev/null +++ b/browserup-proxy-rest-clients/src/test/ruby/Gemfile.lock @@ -0,0 +1,66 @@ +GEM + remote: https://rubygems.org/ + specs: + ast (2.4.0) + byebug (11.0.1) + coderay (1.1.2) + diff-lcs (1.3) + ethon (0.12.0) + ffi (>= 1.3.0) + ffi (1.11.1) + jaro_winkler (1.5.3) + json (2.2.0) + method_source (0.9.2) + openapi_client (1.0.0) + json (~> 2.1, >= 2.1.0) + typhoeus (~> 1.0, >= 1.0.1) + parallel (1.17.0) + parser (2.6.3.0) + ast (~> 2.4.0) + pry (0.12.2) + coderay (~> 1.1.0) + method_source (~> 0.9.0) + pry-byebug (3.7.0) + byebug (~> 11.0) + pry (~> 0.10) + psych (3.1.0) + rainbow (3.0.0) + rake (12.0.0) + rspec (3.8.0) + rspec-core (~> 3.8.0) + rspec-expectations (~> 3.8.0) + rspec-mocks (~> 3.8.0) + rspec-core (3.8.2) + rspec-support (~> 3.8.0) + rspec-expectations (3.8.4) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.8.0) + rspec-mocks (3.8.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.8.0) + rspec-support (3.8.2) + rubocop (0.66.0) + jaro_winkler (~> 1.5.1) + parallel (~> 1.10) + parser (>= 2.5, != 2.5.1.1) + psych (>= 3.1.0) + rainbow (>= 2.2.2, < 4.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 1.4.0, < 1.6) + ruby-progressbar (1.10.1) + typhoeus (1.3.1) + ethon (>= 0.9.0) + unicode-display_width (1.5.0) + +PLATFORMS + ruby + +DEPENDENCIES + openapi_client (~> 1.0.0) + pry-byebug + rake (~> 12.0.0) + rspec + rubocop (~> 0.66.0) + +BUNDLED WITH + 1.15.3 diff --git a/browserup-proxy-rest-clients/src/test/ruby/spec/test_client_spec.rb b/browserup-proxy-rest-clients/src/test/ruby/spec/test_client_spec.rb new file mode 100644 index 000000000..e69618e05 --- /dev/null +++ b/browserup-proxy-rest-clients/src/test/ruby/spec/test_client_spec.rb @@ -0,0 +1,27 @@ +require 'openapi_client' + +describe OpenapiClient do + it 'connects to api' do + proxy_rest_host = ENV["PROXY_REST_HOST"] + proxy_rest_port = ENV["PROXY_REST_PORT"] + proxy_port = ENV["PROXY_PORT"] + + p "Using the following env variables:" + p "PROXY_REST_HOST = #{proxy_rest_host}" + p "PROXY_REST_PORT = #{proxy_rest_port}" + p "PROXY_PORT = #{proxy_port}" + + api_instance = OpenapiClient::DefaultApi.new + api_instance.api_client.config.host = "#{proxy_rest_host}:#{proxy_rest_port}" + port = proxy_port + url_pattern = '^.*$' + + begin + entries_response = api_instance.entries(port, url_pattern).to_json + p "Got the following entries in the response: #{entries_response}" + rescue OpenapiClient::ApiError => e + puts "Exception when calling DefaultApi->entries: #{e}" + raise + end + end +end \ No newline at end of file diff --git a/browserup-proxy-rest/build.gradle b/browserup-proxy-rest/build.gradle index ae5dfeb0e..cd458c4f8 100644 --- a/browserup-proxy-rest/build.gradle +++ b/browserup-proxy-rest/build.gradle @@ -106,3 +106,15 @@ dependencies { testImplementation 'org.awaitility:awaitility:4.0.2' testImplementation 'com.github.tomakehurst:wiremock-jre8:2.24.1' } + +task createVersionProperties() { + doLast { + new File("$buildDir/resources/main/browserup-proxy-rest-version.properties").withWriter { w -> + Properties p = new Properties() + p['version'] = project.version.toString() + p.store w, null + } + } +} + +build.finalizedBy(createVersionProperties) diff --git a/browserup-proxy-rest/src/main/java/com/browserup/bup/proxy/guice/JettyServerProvider.java b/browserup-proxy-rest/src/main/java/com/browserup/bup/proxy/guice/JettyServerProvider.java index 4afe18ba6..7f5b2919d 100644 --- a/browserup-proxy-rest/src/main/java/com/browserup/bup/proxy/guice/JettyServerProvider.java +++ b/browserup-proxy-rest/src/main/java/com/browserup/bup/proxy/guice/JettyServerProvider.java @@ -33,18 +33,18 @@ import javax.servlet.DispatcherType; public class JettyServerProvider implements Provider { - public static final String SWAGGER_CONFIG_NAME = "swagger-config.yaml"; - public static final String SWAGGER_PACKAGE = "com.browserup.bup.rest.resource"; + public static final String OPENAPI_CONFIG_YAML = "openapi-config.yaml"; + public static final String OPENAPI_PACKAGE = "com.browserup.bup.rest.resource"; private Server server; @Inject public JettyServerProvider(@Named("port") int port, @Named("address") String address, MitmProxyManager proxyManager) throws UnknownHostException { OpenApiResource openApiResource = new OpenApiResource(); - openApiResource.setConfigLocation(SWAGGER_CONFIG_NAME); + openApiResource.setConfigLocation(OPENAPI_CONFIG_YAML); ResourceConfig resourceConfig = new ResourceConfig(); - resourceConfig.packages(SWAGGER_PACKAGE); + resourceConfig.packages(OPENAPI_PACKAGE); resourceConfig.register(openApiResource); resourceConfig.register(proxyManagerToHkBinder(proxyManager)); resourceConfig.register(JacksonFeature.class); diff --git a/browserup-proxy-rest/src/main/java/com/browserup/bup/rest/openapi/CustomOpenApiReader.java b/browserup-proxy-rest/src/main/java/com/browserup/bup/rest/openapi/CustomOpenApiReader.java new file mode 100644 index 000000000..a0a58bd8f --- /dev/null +++ b/browserup-proxy-rest/src/main/java/com/browserup/bup/rest/openapi/CustomOpenApiReader.java @@ -0,0 +1,115 @@ +package com.browserup.bup.rest.openapi; + +import com.fasterxml.jackson.annotation.JsonView; +import com.fasterxml.jackson.databind.introspect.AnnotatedMethod; +import io.swagger.v3.jaxrs2.Reader; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.models.ExternalDocumentation; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.parameters.Parameter; +import io.swagger.v3.oas.models.parameters.RequestBody; +import io.swagger.v3.oas.models.responses.ApiResponses; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.servers.Server; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.Consumes; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Method; +import java.util.*; + +/** + * Customizes generation of the following OpenAPI data: + * - OperationID: generates operation id using scheme: {last URL path element of resource}{resource method} + * - Info version: uses version properties file generated while gradle build to get current version of the project + */ +public class CustomOpenApiReader extends Reader { + private static final String VERSION_PROPERTIES_FILE_NAME = "browserup-proxy-rest-version.properties"; + private static final String DEFAULT_VERSION = "2.0.0"; + private static final Logger LOG = LoggerFactory.getLogger(CustomOpenApiReader.class); + + private static String version; + + @Override + protected String getOperationId(String operationId) { + return super.getOperationId(operationId); + } + + @Override + public Operation parseMethod(Method method, List globalParameters, Produces methodProduces, Produces classProduces, Consumes methodConsumes, Consumes classConsumes, List classSecurityRequirements, Optional classExternalDocs, Set classTags, List classServers, boolean isSubresource, RequestBody parentRequestBody, ApiResponses parentResponses, JsonView jsonViewAnnotation, ApiResponse[] classResponses, AnnotatedMethod annotatedMethod) { + Operation operation = super.parseMethod(method, globalParameters, methodProduces, classProduces, methodConsumes, classConsumes, classSecurityRequirements, classExternalDocs, classTags, classServers, isSubresource, parentRequestBody, parentResponses, jsonViewAnnotation, classResponses, annotatedMethod); + + Arrays.stream(method.getDeclaringClass().getAnnotations()) + .filter(Path.class::isInstance) + .filter(path -> StringUtils.isNotEmpty(((Path) path).value())) + .findFirst() + .map(path -> ((Path) path).value()) + .flatMap(this::createOperationIdPrefixByPathAnnotation) + .map(operationIdPrefix -> { + return operationIdPrefix.equals(method.getName()) ? + method.getName() : + operationIdPrefix + StringUtils.capitalize(method.getName()); + }) + .ifPresent(operation::setOperationId); + + return operation; + } + + @Override + public OpenAPI read(Class cls, String parentPath, String parentMethod, boolean isSubresource, RequestBody parentRequestBody, ApiResponses parentResponses, Set parentTags, List parentParameters, Set> scannedResources) { + OpenAPI result = super.read(cls, parentPath, parentMethod, isSubresource, parentRequestBody, parentResponses, parentTags, parentParameters, scannedResources); + result.getInfo().setVersion(getVersion()); + return result; + } + + private Optional createOperationIdPrefixByPathAnnotation(String pathAnnoValue) { + String[] pathElements = pathAnnoValue.split("/"); + if (pathElements.length > 0) { + return Optional.of(pathElements[pathElements.length - 1]); + } + return Optional.empty(); + } + + private String getVersion() { + if (version == null) { + synchronized (CustomOpenApiReader.class) { + if (version == null) { + version = readVersion().orElse(DEFAULT_VERSION); + } + } + } + return version; + } + + private Optional readVersion() { + InputStream in = CustomOpenApiReader.class.getClassLoader().getResourceAsStream(VERSION_PROPERTIES_FILE_NAME); + if (in == null) { + LOG.warn("Couldn't read version properties, resource not found by path: " + VERSION_PROPERTIES_FILE_NAME); + return Optional.empty(); + } + + Properties properties = new Properties(); + try { + properties.load(in); + Object version = properties.get("version"); + if (version == null) { + LOG.warn("Couldn't read version properties (version is null)"); + } else if (!(version instanceof String)) { + LOG.warn("Couldn't read version properties (version is not String)"); + } else if (StringUtils.isEmpty((CharSequence) version)) { + LOG.warn("Couldn't read version properties (version is empty)"); + } else { + return Optional.of((String) version); + } + } catch (IOException e) { + LOG.warn("Couldn't read version properties", e); + } + return Optional.empty(); + } +} diff --git a/browserup-proxy-rest/src/main/java/com/browserup/bup/rest/swagger/DocConstants.java b/browserup-proxy-rest/src/main/java/com/browserup/bup/rest/openapi/DocConstants.java similarity index 98% rename from browserup-proxy-rest/src/main/java/com/browserup/bup/rest/swagger/DocConstants.java rename to browserup-proxy-rest/src/main/java/com/browserup/bup/rest/openapi/DocConstants.java index d2a9f99da..034e734ea 100644 --- a/browserup-proxy-rest/src/main/java/com/browserup/bup/rest/swagger/DocConstants.java +++ b/browserup-proxy-rest/src/main/java/com/browserup/bup/rest/openapi/DocConstants.java @@ -1,4 +1,4 @@ -package com.browserup.bup.rest.swagger; +package com.browserup.bup.rest.openapi; public class DocConstants { public static final String STATUS_DESCRIPTION = "Http status."; diff --git a/browserup-proxy-rest/src/main/java/com/browserup/bup/rest/resource/entries/EntriesProxyResource.java b/browserup-proxy-rest/src/main/java/com/browserup/bup/rest/resource/entries/EntriesProxyResource.java index 114234afa..0e6da6896 100644 --- a/browserup-proxy-rest/src/main/java/com/browserup/bup/rest/resource/entries/EntriesProxyResource.java +++ b/browserup-proxy-rest/src/main/java/com/browserup/bup/rest/resource/entries/EntriesProxyResource.java @@ -1,10 +1,9 @@ package com.browserup.bup.rest.resource.entries; -import com.browserup.bup.BrowserUpProxyServer; import com.browserup.bup.MitmProxyServer; import com.browserup.bup.assertion.model.AssertionResult; import com.browserup.bup.proxy.MitmProxyManager; -import com.browserup.bup.rest.swagger.DocConstants; +import com.browserup.bup.rest.openapi.DocConstants; import com.browserup.bup.rest.validation.HttpStatusCodeConstraint; import com.browserup.bup.rest.validation.LongPositiveConstraint; import com.browserup.bup.rest.validation.NotBlankConstraint; @@ -13,9 +12,11 @@ import com.browserup.bup.rest.validation.PortWithExistingProxyConstraint; import com.browserup.bup.util.HttpStatusClass; import com.browserup.harreader.model.HarEntry; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.info.Info; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -33,8 +34,13 @@ import java.util.Collection; import java.util.regex.Pattern; -import static com.browserup.bup.rest.swagger.DocConstants.*; +import static com.browserup.bup.rest.openapi.DocConstants.*; +@OpenAPIDefinition( + info = @Info( + version = "" + ) +) @Path("/proxy/{port}/har/entries") public class EntriesProxyResource { private static final String URL_PATTERN = "urlPattern"; diff --git a/browserup-proxy-rest/src/main/java/com/browserup/bup/rest/resource/mostrecent/MostRecentEntryProxyResource.java b/browserup-proxy-rest/src/main/java/com/browserup/bup/rest/resource/mostrecent/MostRecentEntryProxyResource.java index d0e513bc0..0d5e6b49e 100644 --- a/browserup-proxy-rest/src/main/java/com/browserup/bup/rest/resource/mostrecent/MostRecentEntryProxyResource.java +++ b/browserup-proxy-rest/src/main/java/com/browserup/bup/rest/resource/mostrecent/MostRecentEntryProxyResource.java @@ -27,10 +27,9 @@ import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; import java.util.regex.Pattern; -import static com.browserup.bup.rest.swagger.DocConstants.*; +import static com.browserup.bup.rest.openapi.DocConstants.*; @Path("/proxy/{port}/har/mostRecentEntry") public class MostRecentEntryProxyResource { diff --git a/browserup-proxy-rest/src/main/resources/swagger-config.yaml b/browserup-proxy-rest/src/main/resources/swagger-config.yaml index 70bc72143..8a0fdbfc7 100644 --- a/browserup-proxy-rest/src/main/resources/swagger-config.yaml +++ b/browserup-proxy-rest/src/main/resources/swagger-config.yaml @@ -1,6 +1,7 @@ resourcePackages: - com.browserup.bup.rest.resource prettyPrint: true +readerClass: com.browserup.bup.rest.openapi.CustomOpenApiReader cacheTTL: 0 openAPI: info: diff --git a/build.gradle b/build.gradle index 5c8419c53..7ebeb31f5 100644 --- a/build.gradle +++ b/build.gradle @@ -21,6 +21,11 @@ configurations.all { } } +ext { + jerseyVersion = '2.29' + guiceVersion = '4.2.2' +} + subprojects { apply plugin: 'java' apply plugin: 'idea' @@ -71,4 +76,4 @@ subprojects { sign configurations.archives } } -} +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 1c6cc46aa..25c37126d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,3 +6,4 @@ include 'browserup-proxy-core' include 'browserup-proxy-dist' include 'browserup-proxy-rest' include 'browserup-proxy-mitm' +include 'browserup-proxy-rest-clients'