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