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 0000000..7321710 Binary files /dev/null and b/test/tls/client-trust.jks differ diff --git a/test/tls/generate-test-cert.sh b/test/tls/generate-test-cert.sh new file mode 100755 index 0000000..eac8ec7 --- /dev/null +++ b/test/tls/generate-test-cert.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# -*- mode: sh -*- +# © 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. + +KEY=server.key +CERT=server.crt +PKCS=server.p12 +PASSWORD=passw0rd + +# Create a private key and certificate in PEM format, for the server to use +openssl req \ + -newkey rsa:2048 -nodes -keyout ${KEY} \ + -subj "/CN=localhost" \ + -x509 -days 365 -out ${CERT} + +# Add the key and certificate to a PKCS #12 key store, for the server to use +openssl pkcs12 \ + -inkey ${KEY} \ + -in ${CERT} \ + -export -out ${PKCS} \ + -password pass:${PASSWORD} + +# Add the certificate to a trust store in JKS format, for Java clients to use when connecting +keytool -import \ + -alias server-cert \ + -file ${CERT} \ + -keystore client-trust.jks \ + -storepass ${PASSWORD} \ + -noprompt diff --git a/test/tls/server.crt b/test/tls/server.crt new file mode 100644 index 0000000..57f2830 --- /dev/null +++ b/test/tls/server.crt @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICpDCCAYwCCQDft9xlN4fNFTANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls +b2NhbGhvc3QwHhcNMTgwMzIwMTUxODMwWhcNMTkwMzIwMTUxODMwWjAUMRIwEAYD +VQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDk +XzX0xQIZzKVX8/lDQh5lSHr5U9cBL+kURA3fEgl3ks9KjZPggfxWl4Y5dekChW/s +iknVssoNw9vI1W25qtQ81zRFQbHbpej0lLdYsS8/yZCuAVjMTp6Q9IswTwhVA6OD +5orag5dH3XQH+GsnmGXRCY7Gs93onAe3i3ShX9qpUFOJXyxCX+pLAC6kWQ3f/HI8 +dujVXKsg1vHgOgGqQGwnh8gm5OeWUeuTMdD2v7Hn1OxilgNMbcewA7bpvipgm2xt +ZD0PKFDmtQ4comr25Oo+eUf1N7jSpRPOWJNxoyS9/coQUPp1Gpbk7khYHjGn7f5a +EZqQ4Hmwwh50uT+vKVxDAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAAHaywC7ZLOi +3PKlidj6PWe33dEVsDL6RRb3cOqR86Ld2aD91oLrpELRhz4v2mt/GfQMIg7rc6z7 +26SuPzV/7zZAv1N/vGoIFyvBXWLYP5qCwUrmykcH/wfFM80S6FJxz5Wy5MA5UzTB +HdpiQCPu4U0IKgATLDraz0xlQ61Rog56YhgJI8ulHuav5iYxqV2mwU09Hs0kXPJ7 +g0PLRaSyidsXafxBKukeM9QHl8z8HN8er23oqecYo59b/Bt0c6jSrJCK39EUcoLP +HxR+Ma1SPhVKGqa3lPmaoAzsFTqaJ6fsIcbp+oEFAq0LPeqMPK7u3ygT4iTblAl8 +q3isCz4Ytx4= +-----END CERTIFICATE----- diff --git a/test/tls/server.key b/test/tls/server.key new file mode 100644 index 0000000..953233d --- /dev/null +++ b/test/tls/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDkXzX0xQIZzKVX +8/lDQh5lSHr5U9cBL+kURA3fEgl3ks9KjZPggfxWl4Y5dekChW/siknVssoNw9vI +1W25qtQ81zRFQbHbpej0lLdYsS8/yZCuAVjMTp6Q9IswTwhVA6OD5orag5dH3XQH ++GsnmGXRCY7Gs93onAe3i3ShX9qpUFOJXyxCX+pLAC6kWQ3f/HI8dujVXKsg1vHg +OgGqQGwnh8gm5OeWUeuTMdD2v7Hn1OxilgNMbcewA7bpvipgm2xtZD0PKFDmtQ4c +omr25Oo+eUf1N7jSpRPOWJNxoyS9/coQUPp1Gpbk7khYHjGn7f5aEZqQ4Hmwwh50 +uT+vKVxDAgMBAAECggEBAL91kybChCBdEcHLKQ7aP+FqAq9FOtwj7qSu6XI7DPTS +gDdgurleQM/X+Q/zaoZSmKMWzQ/79KnVqk2VoYgnUAgx5ACsMxCS59slUxFoetRf +iIxZVLj0sLuWSZsWp0We51eN0Juh9xKo9r435p4rhjDacnjkEwcQyOd4Yy9nzUpk +GDD5Vu1J9bOOKUQZ0qgjPyl/xWiwD1yfGJ0nHpQ5ucfrCO9p+n7SYsx01WcAkC8J +WP9XSXgi5uIefTWb/4m2b32jzjIgzAHkNx6yktRTjBJ7QILnKq1P8JjkNA/Awj4P +OxAz9hHHnVRuq4ZlEqfvo9p9YAbN2IH5TnmN3rGCXwECgYEA9JitVIeXCS0qIMFA +dKCmm9CT7JXccdpVllwaaYCNTb+G2RBrJqAvQEetoYJodWTIm1mNwSEORFFw0W+N +eaMzibJoJ+MZHRhiulDJaY0vwAKHkSJjDPJrPLgGMCUOLiWSAAnR4z35WfeY0e// +JbdZZemrJRyzy3o6rkRN9TQcUMUCgYEA7wTj5w5GZ8NQ7Nn8nIS2ayk+woIMHS+g +RVFufJoBeopsNJfNzGak0s+nz5q0nMGMzQsxXkbmAOLMTU3woQ7cEGjkLAfoch23 +ACOe7M4rZbIk6kVNOlFESWdVdWViVd/B2a7oBqOIykoqX6VSqqrw+xghAUmd/2W1 +uxjg9v01OWcCgYApE5LYRUUKF3mhspKeg3Q3apnM+4Xf4OjKrYEKArq4OdftkCJO +hEwrIV55Zysfu+Mso6d4rZJ1yq+FnJRHvy6ii0GOoUbQag36eCK7BSjluAcISpwT +yopT0hvH7hEpksmoE/4ZiYjcoQYbC5DvxpDO2qURQHa5TzeXmIT3Dt9KeQKBgQC6 +UKeOXrRHAhs85ZdiMpk340jGujTTM2LNZfKoMixg5zH9tS9427IzmicHT2LmpoEo +/EaZZM65dhEnWU/vW/Py3rCuGeP5wGv8Mcgac4OknD7mVusiQGLojSIyhrsmkWs8 +UnkPY76nYTSypd5Qpzt9n4tqw4XjpdcJZxVFso8glQKBgQCHlb15As73En/Q2AxL +5FY1Q1lLuO8y33ZZIRK4eynOKkbiuAh7X+ONZ4T9NtTm2J7mnltvTHZ7yeOI+VLS +LrTTBwnnNfdpp8UVPQlwzeizoDqSbr1sjFYvKOfdDDfxuzieT/4tfW9VTAxn4uOg +qpg7aRMUYUuLAH+S5atdOqXB+g== +-----END PRIVATE KEY----- diff --git a/test/tls/server.p12 b/test/tls/server.p12 new file mode 100644 index 0000000..a859a4c Binary files /dev/null and b/test/tls/server.p12 differ