From ab3b44e84bd8c5e0e5ad9045cc526a4b887b44b3 Mon Sep 17 00:00:00 2001 From: Arthur Barr Date: Tue, 20 Mar 2018 16:47:24 +0000 Subject: [PATCH] TLS and HTTPS configuration in default developer config --- cmd/runmqdevserver/keystore.go | 136 +++++++++++++++++ cmd/runmqdevserver/main.go | 13 ++ cmd/runmqdevserver/mqsc.go | 2 +- cmd/runmqdevserver/tls.go | 140 +++++++++++++++++ cmd/runmqserver/post_init_dev.go | 26 +++- cmd/runmqserver/webserver.go | 27 +++- .../{dev.mqsc.tpl => 10-dev.mqsc.tpl} | 6 +- incubating/mqadvanced-server-dev/Dockerfile | 8 +- .../Installation1/servers/mqweb/tls-dev.xml | 9 +- .../Installation1/servers/mqweb/tls.xml | 3 + test/docker/devconfig_test.go | 102 +++++++++---- test/docker/devconfig_test_util.go | 106 ++++++++++++- test/messaging/.gitignore | 6 + test/messaging/Dockerfile | 38 +++++ test/messaging/build.gradle | 39 +++++ .../com/ibm/mqcontainer/test/JMSTests.java | 144 ++++++++++++++++++ test/tls/client-trust.jks | Bin 0 -> 748 bytes test/tls/generate-test-cert.sh | 41 +++++ test/tls/server.crt | 17 +++ test/tls/server.key | 28 ++++ test/tls/server.p12 | Bin 0 -> 2285 bytes 21 files changed, 849 insertions(+), 42 deletions(-) create mode 100644 cmd/runmqdevserver/keystore.go create mode 100644 cmd/runmqdevserver/tls.go rename incubating/mqadvanced-server-dev/{dev.mqsc.tpl => 10-dev.mqsc.tpl} (90%) create mode 100644 test/messaging/.gitignore create mode 100644 test/messaging/Dockerfile create mode 100644 test/messaging/build.gradle create mode 100644 test/messaging/src/main/java/com/ibm/mqcontainer/test/JMSTests.java create mode 100644 test/tls/client-trust.jks create mode 100755 test/tls/generate-test-cert.sh create mode 100644 test/tls/server.crt create mode 100644 test/tls/server.key create mode 100644 test/tls/server.p12 diff --git a/cmd/runmqdevserver/keystore.go b/cmd/runmqdevserver/keystore.go new file mode 100644 index 0000000..1b49d68 --- /dev/null +++ b/cmd/runmqdevserver/keystore.go @@ -0,0 +1,136 @@ +/* +© Copyright IBM Corporation 2018 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package main + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/ibm-messaging/mq-container/internal/command" +) + +type KeyStore struct { + Filename string + Password string + keyStoreType string + command string +} + +// NewJKSKeyStore creates a new Java Key Store, managed by the runmqckm command +func NewJKSKeyStore(filename, password string) *KeyStore { + return &KeyStore{ + Filename: filename, + Password: password, + keyStoreType: "jks", + command: "/opt/mqm/bin/runmqckm", + } +} + +// NewCMSKeyStore creates a new MQ CMS Key Store, managed by the runmqakm command +func NewCMSKeyStore(filename, password string) *KeyStore { + return &KeyStore{ + Filename: filename, + Password: password, + keyStoreType: "cms", + command: "/opt/mqm/bin/runmqakm", + } +} + +// Create a key store, if it doesn't already exist +func (ks *KeyStore) Create() error { + _, err := os.Stat(ks.Filename) + if err != nil { + if os.IsNotExist(err) { + _, _, err := command.Run(ks.command, "-keydb", "-create", "-type", ks.keyStoreType, "-db", ks.Filename, "-pw", ks.Password, "-stash") + if err != nil { + return fmt.Errorf("error running \"%v -keydb -create\": %v", ks.command, err) + } + } + } + // TODO: Lookup value for MQM user here? + err = os.Chown(ks.Filename, 999, 999) + if err != nil { + log.Error(err) + return err + } + return nil +} + +// Create a key stash, if it doesn't already exist +func (ks *KeyStore) CreateStash() error { + extension := filepath.Ext(ks.Filename) + stashFile := ks.Filename[0:len(ks.Filename)-len(extension)] + ".sth" + log.Debugf("TLS stash file: %v", stashFile) + _, err := os.Stat(stashFile) + if err != nil { + if os.IsNotExist(err) { + _, _, err := command.Run(ks.command, "-keydb", "-stashpw", "-type", ks.keyStoreType, "-db", ks.Filename, "-pw", ks.Password) + if err != nil { + return fmt.Errorf("error running \"%v -keydb -stashpw\": %v", ks.command, err) + } + } + return err + } + // TODO: Lookup value for MQM user here? + err = os.Chown(stashFile, 999, 999) + if err != nil { + log.Error(err) + return err + } + return nil +} + +func (ks *KeyStore) Import(inputFile, password string) error { + _, _, err := command.Run(ks.command, "-cert", "-import", "-file", inputFile, "-pw", password, "-target", ks.Filename, "-target_pw", ks.Password, "-target_type", ks.keyStoreType) + if err != nil { + return fmt.Errorf("error running \"%v -cert -import\": %v", ks.command, err) + } + return nil +} + +// GetCertificateLabels returns the labels of all certificates in the key store +func (ks *KeyStore) GetCertificateLabels() ([]string, error) { + out, _, err := command.Run(ks.command, "-cert", "-list", "-type", ks.keyStoreType, "-db", ks.Filename, "-pw", ks.Password) + if err != nil { + return nil, fmt.Errorf("error running \"%v -cert -list\": %v", ks.command, err) + } + scanner := bufio.NewScanner(strings.NewReader(out)) + var labels []string + for scanner.Scan() { + s := scanner.Text() + if strings.HasPrefix(s, "-") || strings.HasPrefix(s, "*-") { + s := strings.TrimLeft(s, "-*") + labels = append(labels, strings.TrimSpace(s)) + } + } + err = scanner.Err() + if err != nil { + return nil, err + } + return labels, nil +} + +// RenameCertificate renames the specified certificate +func (ks *KeyStore) RenameCertificate(from, to string) error { + _, _, err := command.Run(ks.command, "-cert", "-rename", "-db", ks.Filename, "-pw", ks.Password, "-label", from, "-new_label", to) + if err != nil { + return fmt.Errorf("error running \"%v -cert -rename\": %v", ks.command, err) + } + return nil +} diff --git a/cmd/runmqdevserver/main.go b/cmd/runmqdevserver/main.go index 28189b3..61b03d1 100644 --- a/cmd/runmqdevserver/main.go +++ b/cmd/runmqdevserver/main.go @@ -129,6 +129,19 @@ func doMain() error { return err } + name, err := name.GetQueueManagerName() + if err != nil { + logTerminationf("Error getting queue manager name: %v", err) + } + ks, set := os.LookupEnv("MQ_TLS_KEYSTORE") + if set { + err = configureTLS(name, ks, os.Getenv("MQ_TLS_PASSPHRASE")) + if err != nil { + logTerminationf("Error configuring TLS: %v", err) + return err + } + } + return nil } diff --git a/cmd/runmqdevserver/mqsc.go b/cmd/runmqdevserver/mqsc.go index a30e267..e3ba0ba 100644 --- a/cmd/runmqdevserver/mqsc.go +++ b/cmd/runmqdevserver/mqsc.go @@ -27,7 +27,7 @@ func updateMQSC(appPasswordRequired bool) error { } else { checkClient = "ASQMGR" } - const mqsc string = "/etc/mqm/dev.mqsc" + const mqsc string = "/etc/mqm/10-dev.mqsc" if os.Getenv("MQ_DEV") == "true" { const mqscTemplate string = mqsc + ".tpl" // Re-configure channel if app password not set diff --git a/cmd/runmqdevserver/tls.go b/cmd/runmqdevserver/tls.go new file mode 100644 index 0000000..26cdb34 --- /dev/null +++ b/cmd/runmqdevserver/tls.go @@ -0,0 +1,140 @@ +/* +© Copyright IBM Corporation 2018 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package main + +import ( + "fmt" + "os" + "path/filepath" +) + +func configureWebTLS(cms *KeyStore) error { + dir := "/run/runmqdevserver/tls" + ks := NewJKSKeyStore(filepath.Join(dir, "key.jks"), cms.Password) + ts := NewJKSKeyStore(filepath.Join(dir, "trust.jks"), cms.Password) + + log.Debug("Creating key store") + err := ks.Create() + if err != nil { + return err + } + log.Debug("Creating trust store") + err = ts.Create() + if err != nil { + return err + } + log.Debug("Importing keys") + err = ks.Import(cms.Filename, cms.Password) + if err != nil { + return err + } + + webConfigDir := "/etc/mqm/web/installations/Installation1/servers/mqweb" + tlsConfig := filepath.Join(webConfigDir, "tls.xml") + newTLSConfig := filepath.Join(webConfigDir, "tls-dev.xml") + err = os.Remove(tlsConfig) + if err != nil { + return err + } + err = os.Rename(newTLSConfig, tlsConfig) + if err != nil { + return err + } + + return nil +} + +func configureTLS(qmName string, inputFile string, passPhrase string) error { + log.Debug("Configuring TLS") + + _, err := os.Stat(inputFile) + if err != nil { + return err + } + + // TODO: Use a persisted file (on the volume) instead? + dir := "/run/runmqdevserver/tls" + keyFile := filepath.Join(dir, "key.kdb") + + _, err = os.Stat(dir) + if err != nil { + if os.IsNotExist(err) { + err = os.MkdirAll(dir, 0770) + if err != nil { + return err + } + err = os.Chown(dir, 999, 999) + if err != nil { + log.Debug(err) + return err + } + } else { + return err + } + } + + cms := NewCMSKeyStore(keyFile, passPhrase) + + err = cms.Create() + if err != nil { + return err + } + + err = cms.CreateStash() + if err != nil { + return err + } + + err = cms.Import(inputFile, passPhrase) + if err != nil { + return err + } + + labels, err := cms.GetCertificateLabels() + if err != nil { + return err + } + if len(labels) == 0 { + return fmt.Errorf("unable to find certificate label") + } + log.Debugf("Renaming certificate from %v", labels[0]) + const newLabel string = "devcert" + err = cms.RenameCertificate(labels[0], newLabel) + if err != nil { + return err + } + + if os.Getenv("MQ_DEV") == "true" { + f, err := os.OpenFile("/etc/mqm/20-dev-tls.mqsc", os.O_WRONLY|os.O_CREATE, 0770) + if err != nil { + return err + } + defer f.Close() + // Change the Queue Manager's Key Repository to point at the new TLS key store + fmt.Fprintf(f, "ALTER QMGR SSLKEYR('%s')\n", filepath.Join(dir, "key")) + fmt.Fprintf(f, "ALTER QMGR CERTLABL('%s')\n", newLabel) + // Alter the DEV channels to use TLS + fmt.Fprintln(f, "ALTER CHANNEL('DEV.APP.SVRCONN') CHLTYPE(SVRCONN) SSLCIPH(TLS_RSA_WITH_AES_128_CBC_SHA256) SSLCAUTH(OPTIONAL)") + fmt.Fprintln(f, "ALTER CHANNEL('DEV.ADMIN.SVRCONN') CHLTYPE(SVRCONN) SSLCIPH(TLS_RSA_WITH_AES_128_CBC_SHA256) SSLCAUTH(OPTIONAL)") + } + + err = configureWebTLS(cms) + if err != nil { + return err + } + + return nil +} diff --git a/cmd/runmqserver/post_init_dev.go b/cmd/runmqserver/post_init_dev.go index ddbe7f1..daaba55 100644 --- a/cmd/runmqserver/post_init_dev.go +++ b/cmd/runmqserver/post_init_dev.go @@ -17,7 +17,10 @@ limitations under the License. */ package main -import "os" +import ( + "os" + "path/filepath" +) // postInit is run after /var/mqm is set up // This version of postInit is only included as part of the MQ Advanced for Developers build @@ -32,5 +35,26 @@ func postInit(name string) error { startWebServer() }() } + + dir := "/etc/mqm/tls" + keyFile := filepath.Join(dir, "key.kdb") + stashFile := filepath.Join(dir, "key.sth") + + _, err := os.Stat(keyFile) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + + _, err = os.Stat(stashFile) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + return nil } diff --git a/cmd/runmqserver/webserver.go b/cmd/runmqserver/webserver.go index 2224b45..e670cb4 100644 --- a/cmd/runmqserver/webserver.go +++ b/cmd/runmqserver/webserver.go @@ -19,6 +19,7 @@ package main import ( "fmt" + "io" "os" "path/filepath" @@ -41,6 +42,26 @@ func startWebServer() error { return nil } +// CopyFile copies the specified file +func CopyFile(src, dest string) error { + log.Debugf("Copying file %v to %v", src, dest) + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + out, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY, 0770) + defer out.Close() + + _, err = io.Copy(out, in) + if err != nil { + return err + } + err = out.Close() + return err +} + func configureWebServer() error { _, err := os.Stat("/opt/mqm/bin/strmqweb") if err != nil { @@ -76,7 +97,6 @@ func configureWebServer() error { return err } } - if info.IsDir() { if !exists { err := os.MkdirAll(to, 0770) @@ -84,7 +104,6 @@ func configureWebServer() error { return err } } - log.Printf("Directory: %v --> %v", from, to) } else { if exists { err := os.Remove(to) @@ -92,13 +111,11 @@ func configureWebServer() error { return err } } - // TODO: Permissions. Can't rely on them being set in Dockerfile - err := os.Link(from, to) + err := CopyFile(from, to) if err != nil { log.Debug(err) return err } - log.Printf("File: %v", from) } err = os.Chown(to, uid, gid) if err != nil { diff --git a/incubating/mqadvanced-server-dev/dev.mqsc.tpl b/incubating/mqadvanced-server-dev/10-dev.mqsc.tpl similarity index 90% rename from incubating/mqadvanced-server-dev/dev.mqsc.tpl rename to incubating/mqadvanced-server-dev/10-dev.mqsc.tpl index 3dc733f..a544849 100644 --- a/incubating/mqadvanced-server-dev/dev.mqsc.tpl +++ b/incubating/mqadvanced-server-dev/10-dev.mqsc.tpl @@ -32,9 +32,10 @@ DEFINE AUTHINFO('DEV.AUTHINFO') AUTHTYPE(IDPWOS) CHCKCLNT(REQDADM) CHCKLOCL(OPTI ALTER QMGR CONNAUTH('DEV.AUTHINFO') REFRESH SECURITY(*) TYPE(CONNAUTH) +* Developer channels (Application + Admin) * Developer channels (Application + Admin) DEFINE CHANNEL('DEV.ADMIN.SVRCONN') CHLTYPE(SVRCONN) REPLACE -DEFINE CHANNEL('DEV.APP.SVRCONN') CHLTYPE(SVRCONN) REPLACE +DEFINE CHANNEL('DEV.APP.SVRCONN') CHLTYPE(SVRCONN) MCAUSER('app') REPLACE * Developer channel authentication rules SET CHLAUTH('*') TYPE(ADDRESSMAP) ADDRESS('*') USERSRC(NOACCESS) DESCR('Back-stop rule - Blocks everyone') ACTION(REPLACE) @@ -43,8 +44,7 @@ SET CHLAUTH('DEV.ADMIN.SVRCONN') TYPE(BLOCKUSER) USERLIST('nobody') DESCR('Allow SET CHLAUTH('DEV.ADMIN.SVRCONN') TYPE(USERMAP) CLNTUSER('admin') USERSRC(CHANNEL) DESCR('Allows admin user to connect via ADMIN channel') ACTION(REPLACE) * Developer authority records -SET AUTHREC PROFILE('DEV.AUTHINFO') GROUP('root') OBJTYPE(AUTHINFO) AUTHADD(CHG,DLT,DSP,INQ) -SET AUTHREC PROFILE('DEV.AUTHINFO') GROUP('mqm') OBJTYPE(AUTHINFO) AUTHADD(CHG,DLT,DSP,INQ) +SET AUTHREC PROFILE('self') GROUP('mqclient') OBJTYPE(QMGR) AUTHADD(CONNECT,INQ) SET AUTHREC PROFILE('DEV.**') GROUP('mqclient') OBJTYPE(QUEUE) AUTHADD(BROWSE,GET,INQ,PUT) SET AUTHREC PROFILE('DEV.**') GROUP('mqclient') OBJTYPE(TOPIC) AUTHADD(PUB,SUB) diff --git a/incubating/mqadvanced-server-dev/Dockerfile b/incubating/mqadvanced-server-dev/Dockerfile index bf0ef99..ab660e7 100644 --- a/incubating/mqadvanced-server-dev/Dockerfile +++ b/incubating/mqadvanced-server-dev/Dockerfile @@ -40,13 +40,17 @@ ENV MQ_ADMIN_PASSWORD=passw0rd ## Add admin and app users, and set a default password for admin RUN useradd admin -G mqm \ && groupadd mqclient \ - && useradd app -G mqclient,mqm \ + && useradd app -G mqclient \ && echo admin:$MQ_ADMIN_PASSWORD | chpasswd +# Create a directory for runtime data from runmqserver +RUN mkdir -p /run/runmqdevserver \ + && chown mqm:mqm /run/runmqdevserver + COPY --from=builder /go/src/github.com/ibm-messaging/mq-container/runmqserver /usr/local/bin/ COPY --from=builder /go/src/github.com/ibm-messaging/mq-container/runmqdevserver /usr/local/bin/ # Copy template MQSC for default developer configuration -COPY incubating/mqadvanced-server-dev/dev.mqsc.tpl /etc/mqm/ +COPY incubating/mqadvanced-server-dev/10-dev.mqsc.tpl /etc/mqm/ # Copy web XML files for default developer configuration COPY incubating/mqadvanced-server-dev/web /etc/mqm/web RUN chmod +x /usr/local/bin/runmq* diff --git a/incubating/mqadvanced-server-dev/web/installations/Installation1/servers/mqweb/tls-dev.xml b/incubating/mqadvanced-server-dev/web/installations/Installation1/servers/mqweb/tls-dev.xml index 970941b..903656c 100644 --- a/incubating/mqadvanced-server-dev/web/installations/Installation1/servers/mqweb/tls-dev.xml +++ b/incubating/mqadvanced-server-dev/web/installations/Installation1/servers/mqweb/tls-dev.xml @@ -1,4 +1,7 @@ - - - + + + + + + \ No newline at end of file diff --git a/incubating/mqadvanced-server-dev/web/installations/Installation1/servers/mqweb/tls.xml b/incubating/mqadvanced-server-dev/web/installations/Installation1/servers/mqweb/tls.xml index 395b67f..c383ada 100644 --- a/incubating/mqadvanced-server-dev/web/installations/Installation1/servers/mqweb/tls.xml +++ b/incubating/mqadvanced-server-dev/web/installations/Installation1/servers/mqweb/tls.xml @@ -1 +1,4 @@ + + + \ No newline at end of file diff --git a/test/docker/devconfig_test.go b/test/docker/devconfig_test.go index 4c8ddd4..e0daa76 100644 --- a/test/docker/devconfig_test.go +++ b/test/docker/devconfig_test.go @@ -18,14 +18,15 @@ limitations under the License. package main import ( + "context" "crypto/tls" - "fmt" - "net/http" + "path/filepath" "testing" - "time" "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/network" "github.com/docker/docker/client" + "github.com/docker/go-connections/nat" ) func TestDevGoldenPath(t *testing.T) { @@ -35,7 +36,12 @@ func TestDevGoldenPath(t *testing.T) { t.Fatal(err) } containerConfig := container.Config{ - Env: []string{"LICENSE=accept", "MQ_QMGR_NAME=qm1"}, + Env: []string{ + "LICENSE=accept", + "MQ_QMGR_NAME=qm1", + // TODO: Use default password (not set) here + "MQ_APP_PASSWORD=" + devAppPassword, + }, } id := runContainer(t, cli, &containerConfig) @@ -43,30 +49,74 @@ func TestDevGoldenPath(t *testing.T) { waitForReady(t, cli, id) waitForWebReady(t, cli, id) - timeout := time.Duration(30 * time.Second) - // Disable TLS verification (server uses a self-signed certificate by default, - // so verification isn't useful anyway) - tr := &http.Transport{ - TLSClientConfig: &tls.Config{ + t.Run("REST", func(t *testing.T) { + // Disable TLS verification (server uses a self-signed certificate by default, + // so verification isn't useful anyway) + testREST(t, cli, id, &tls.Config{ InsecureSkipVerify: true, - }, - } - httpClient := http.Client{ - Timeout: timeout, - Transport: tr, - } - - url := fmt.Sprintf("https://localhost:%s/ibmmq/rest/v1/admin/installation", getWebPort(t, cli, id)) - req, err := http.NewRequest("GET", url, nil) - req.SetBasicAuth("admin", "passw0rd") - resp, err := httpClient.Do(req) - if err != nil { - t.Fatal(err) - } - if resp.StatusCode != http.StatusOK { - t.Errorf("Expected HTTP status code %v from 'GET installation'; got %v", http.StatusOK, resp.StatusCode) - } + }) + }) + t.Run("JMS", func(t *testing.T) { + runJMSTests(t, cli, id, false) + }) // Stop the container cleanly stopContainer(t, cli, id) } + +func TestDevTLS(t *testing.T) { + t.Parallel() + cli, err := client.NewEnvClient() + if err != nil { + t.Fatal(err) + } + const tlsPassPhrase string = "passw0rd" + containerConfig := container.Config{ + Env: []string{ + "LICENSE=accept", + "MQ_QMGR_NAME=qm1", + "MQ_APP_PASSWORD=" + devAppPassword, + "MQ_TLS_KEYSTORE=/var/tls/server.p12", + "MQ_TLS_PASSPHRASE=" + tlsPassPhrase, + "DEBUG=1", + }, + Image: imageName(), + } + hostConfig := container.HostConfig{ + Binds: []string{ + coverageBind(t), + tlsDir(t) + ":/var/tls", + }, + // Assign a random port for the web server on the host + // TODO: Don't do this for all tests + PortBindings: nat.PortMap{ + "9443/tcp": []nat.PortBinding{ + { + HostIP: "0.0.0.0", + }, + }, + }, + } + networkingConfig := network.NetworkingConfig{} + ctr, err := cli.ContainerCreate(context.Background(), &containerConfig, &hostConfig, &networkingConfig, t.Name()) + if err != nil { + t.Fatal(err) + } + + defer cleanContainer(t, cli, ctr.ID) + startContainer(t, cli, ctr.ID) + waitForReady(t, cli, ctr.ID) + waitForWebReady(t, cli, ctr.ID) + + t.Run("REST", func(t *testing.T) { + // Use the correct certificate for the HTTPS connection + cert := filepath.Join(tlsDir(t), "server.crt") + testREST(t, cli, ctr.ID, createTLSConfig(t, cert, tlsPassPhrase)) + }) + t.Run("JMS", func(t *testing.T) { + runJMSTests(t, cli, ctr.ID, true) + }) + + // Stop the container cleanly + stopContainer(t, cli, ctr.ID) +} diff --git a/test/docker/devconfig_test_util.go b/test/docker/devconfig_test_util.go index e3051cb..70bea94 100644 --- a/test/docker/devconfig_test_util.go +++ b/test/docker/devconfig_test_util.go @@ -18,14 +18,26 @@ limitations under the License. package main import ( + "context" "crypto/tls" + "crypto/x509" "fmt" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strings" "testing" "time" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/network" "github.com/docker/docker/client" ) +const devAdminPassword string = "passw0rd" +const devAppPassword string = "passw0rd" + func waitForWebReady(t *testing.T, cli *client.Client, ID string) { config := tls.Config{InsecureSkipVerify: true} a := fmt.Sprintf("localhost:%s", getWebPort(t, cli, ID)) @@ -34,9 +46,101 @@ func waitForWebReady(t *testing.T, cli *client.Client, ID string) { if err == nil { conn.Close() // Extra sleep to allow web apps to start - time.Sleep(3 * time.Second) + time.Sleep(5 * time.Second) t.Log("MQ web server is ready") return } + time.Sleep(1 * time.Second) + } +} + +// tlsDir returns the host directory where the test certificate(s) are located +func tlsDir(t *testing.T) string { + dir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + return filepath.Join(dir, "../tls") +} + +// runJMSTests runs a container with a JMS client, which connects to the queue manager container with the specified ID +func runJMSTests(t *testing.T, cli *client.Client, ID string, tls bool) { + containerConfig := container.Config{ + // -e MQ_PORT_1414_TCP_ADDR=9.145.14.173 -e MQ_USERNAME=app -e MQ_PASSWORD=passw0rd -e MQ_CHANNEL=DEV.APP.SVRCONN -e MQ_TLS_KEYSTORE=/tls/test.p12 -e MQ_TLS_PASSPHRASE=passw0rd -v /Users/arthurbarr/go/src/github.com/ibm-messaging/mq-container/test/tls:/tls msgtest + Env: []string{ + "MQ_PORT_1414_TCP_ADDR=" + getIPAddress(t, cli, ID), + "MQ_USERNAME=app", + "MQ_PASSWORD=" + devAppPassword, + "MQ_CHANNEL=DEV.APP.SVRCONN", + }, + Image: "msgtest", + } + if tls { + t.Log("Using TLS from JMS client") + containerConfig.Env = append(containerConfig.Env, []string{ + "MQ_TLS_TRUSTSTORE=/var/tls/client-trust.jks", + "MQ_TLS_PASSPHRASE=passw0rd", + }...) + } + hostConfig := container.HostConfig{ + Binds: []string{ + coverageBind(t), + tlsDir(t) + ":/var/tls", + }, + } + networkingConfig := network.NetworkingConfig{} + ctr, err := cli.ContainerCreate(context.Background(), &containerConfig, &hostConfig, &networkingConfig, strings.Replace(t.Name(), "/", "", -1)) + if err != nil { + t.Fatal(err) + } + startContainer(t, cli, ctr.ID) + rc := waitForContainer(t, cli, ctr.ID, 10) + if rc != 0 { + t.Errorf("JUnit container failed with rc=%v", rc) + } + defer cleanContainer(t, cli, ctr.ID) +} + +// createTLSConfig creates a tls.Config which trusts the specified certificate +func createTLSConfig(t *testing.T, certFile, password string) *tls.Config { + // Get the SystemCertPool, continue with an empty pool on error + certs, err := x509.SystemCertPool() + if err != nil { + t.Fatal(err) + } + // Read in the cert file + cert, err := ioutil.ReadFile(certFile) + if err != nil { + t.Fatal(err) + } + // Append our cert to the system pool + ok := certs.AppendCertsFromPEM(cert) + if !ok { + t.Fatal("No certs appended") + } + // Trust the augmented cert pool in our client + return &tls.Config{ + InsecureSkipVerify: false, + RootCAs: certs, + } +} + +func testREST(t *testing.T, cli *client.Client, ID string, tlsConfig *tls.Config) { + httpClient := http.Client{ + Timeout: time.Duration(30 * time.Second), + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, + } + + url := fmt.Sprintf("https://localhost:%s/ibmmq/rest/v1/admin/installation", getWebPort(t, cli, ID)) + req, err := http.NewRequest("GET", url, nil) + req.SetBasicAuth("admin", devAdminPassword) + resp, err := httpClient.Do(req) + if err != nil { + t.Fatal(err) + } + if resp.StatusCode != http.StatusOK { + t.Errorf("Expected HTTP status code %v from 'GET installation'; got %v", http.StatusOK, resp.StatusCode) } } diff --git a/test/messaging/.gitignore b/test/messaging/.gitignore new file mode 100644 index 0000000..b04e6bf --- /dev/null +++ b/test/messaging/.gitignore @@ -0,0 +1,6 @@ +.classpath +.gradle +.project +.settings +bin + diff --git a/test/messaging/Dockerfile b/test/messaging/Dockerfile new file mode 100644 index 0000000..c18934f --- /dev/null +++ b/test/messaging/Dockerfile @@ -0,0 +1,38 @@ +# © Copyright IBM Corporation 2018 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +############################################################################### +# Application build environment (Gradle) +############################################################################### +FROM gradle as builder +ENV GRADLE_OPTS="-Dorg.gradle.daemon=false" +# Change where Gradle stores its cache, so that it's not under a volume +# (and therefore gets cached by Docker) +ENV GRADLE_USER_HOME=/home/gradle/gradle +RUN mkdir -p $GRADLE_USER_HOME +COPY --chown=gradle build.gradle /app/ +WORKDIR /app +# Download dependencies separately, so Docker caches them +RUN gradle download +# Copy source +COPY --chown=gradle src /app/src +# Run the main build +RUN gradle install + +############################################################################### +# Application runtime (JRE only, no build environment) +############################################################################### +FROM ibmjava:sfj +COPY --from=builder /app/lib/*.jar /opt/app/ +ENTRYPOINT ["java", "-classpath", "/opt/app/*", "org.junit.platform.console.ConsoleLauncher", "-p", "com.ibm.mqcontainer.test", "--details", "verbose"] diff --git a/test/messaging/build.gradle b/test/messaging/build.gradle new file mode 100644 index 0000000..22700e9 --- /dev/null +++ b/test/messaging/build.gradle @@ -0,0 +1,39 @@ +# © Copyright IBM Corporation 2018 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apply plugin: 'java' + +repositories { + mavenCentral() +} + +dependencies { + compile group: 'com.ibm.mq', name: 'com.ibm.mq.allclient', version: '9.0.4.0' + compile "org.junit.jupiter:junit-jupiter-api:5.0.3" + runtime "org.junit.jupiter:junit-jupiter-engine:5.0.3" + runtime "org.junit.platform:junit-platform-console-standalone:1.0.3" +} + +task download(type: Exec) { + configurations.runtime.files + commandLine 'echo', 'Downloaded all dependencies' +} + +// Copy all dependencies to the lib directory +task install(type: Copy) { + dependsOn build + from configurations.runtime + from jar + into "${project.projectDir}/lib" +} \ No newline at end of file diff --git a/test/messaging/src/main/java/com/ibm/mqcontainer/test/JMSTests.java b/test/messaging/src/main/java/com/ibm/mqcontainer/test/JMSTests.java new file mode 100644 index 0000000..44f5d4e --- /dev/null +++ b/test/messaging/src/main/java/com/ibm/mqcontainer/test/JMSTests.java @@ -0,0 +1,144 @@ +/* +© Copyright IBM Corporation 2018 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package com.ibm.mqcontainer.test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.FileInputStream; +import java.io.IOException; +import java.net.Socket; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.util.logging.Logger; + +import javax.jms.JMSContext; +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.Queue; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManagerFactory; + +import com.ibm.mq.jms.MQConnectionFactory; +import com.ibm.mq.jms.MQQueue; +import com.ibm.msg.client.wmq.WMQConstants; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; + +class JMSTests { + private static final Logger LOGGER = Logger.getLogger(JMSTests.class.getName()); + protected static final String ADDR = System.getenv("MQ_PORT_1414_TCP_ADDR"); + protected static final String USER = System.getenv("MQ_USERNAME"); + protected static final String PASSWORD = System.getenv("MQ_PASSWORD"); + protected static final String CHANNEL = System.getenv("MQ_CHANNEL"); + protected static final String TRUSTSTORE = System.getenv("MQ_TLS_TRUSTSTORE"); + protected static final String PASSPHRASE = System.getenv("MQ_TLS_PASSPHRASE"); + private JMSContext context; + + static SSLSocketFactory createSSLSocketFactory() throws IOException, GeneralSecurityException { + KeyStore ts=KeyStore.getInstance("jks"); + ts.load(new FileInputStream(TRUSTSTORE), PASSPHRASE.toCharArray()); + // KeyManagerFactory kmf=KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + TrustManagerFactory tmf=TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(ts); + // tmf.init(); + SSLContext ctx = SSLContext.getInstance("TLSv1.2"); + // Security.setProperty("crypto.policy", "unlimited"); + ctx.init(null, tmf.getTrustManagers(), null); + return ctx.getSocketFactory(); + } + + static JMSContext create(String channel, String addr, String user, String password) throws JMSException, IOException, GeneralSecurityException { + LOGGER.info(String.format("Connecting to %s/TCP/%s(1414) as %s", channel, addr, user)); + MQConnectionFactory factory = new MQConnectionFactory(); + factory.setTransportType(WMQConstants.WMQ_CM_CLIENT); + factory.setChannel(channel); + factory.setConnectionNameList(String.format("%s(1414)", addr)); + // factory.setClientReconnectOptions(WMQConstants.WMQ_CLIENT_RECONNECT); + if (TRUSTSTORE == null) { + LOGGER.info("Not using TLS"); + } + else { + LOGGER.info(String.format("Using TLS. Trust store=%s", TRUSTSTORE)); + SSLSocketFactory ssl = createSSLSocketFactory(); + factory.setSSLSocketFactory(ssl); + factory.setSSLCipherSuite("SSL_RSA_WITH_AES_128_CBC_SHA256"); + // LOGGER.info(Arrays.toString(ssl.getSupportedCipherSuites())); + } + // Give up if unable to reconnect for 10 minutes + // factory.setClientReconnectTimeout(600); + // LOGGER.info(String.format("user=%s pw=%s", user, password)); + return factory.createContext(user, password); + } + + @BeforeAll + private static void waitForQueueManager() { + for (int i = 0; i < 20; i++) { + try { + Socket s = new Socket(ADDR, 1414); + s.close(); + return; + } catch (IOException e) { + try { + Thread.sleep(500); + } catch (InterruptedException ex) { + } + } + } + } + + @BeforeEach + void connect() throws Exception { + context = create(CHANNEL, ADDR, USER, PASSWORD); + } + + @Test + void succeedingTest(TestInfo t) throws JMSException { + Queue queue = new MQQueue("DEV.QUEUE.1"); + context.createProducer().send(queue, t.getDisplayName()); + Message m = context.createConsumer(queue).receive(); + assertNotNull(m.getBody(String.class)); + } + + // @Test + // void failingTest() { + // fail("a failing test"); + // } + + @Test + @Disabled("for demonstration purposes") + void skippedTest() { + // not executed + } + + @AfterEach + void tearDown() { + if (context != null) { + context.close(); + } + } + + @AfterAll + static void tearDownAll() { + } + +} \ No newline at end of file diff --git a/test/tls/client-trust.jks b/test/tls/client-trust.jks new file mode 100644 index 0000000000000000000000000000000000000000..7321710faaca487232b158e9b46f8f79333dd082 GIT binary patch literal 748 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 Ca3V?o literal 0 HcmV?d00001