diff --git a/.gitignore b/.gitignore index e99be82..071dada 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,10 @@ dist /build *.exe +# local conf files +etc/*.pem +etc/*.json + # Only exists if Bazel was run /bazel-out diff --git a/client/src/services/websocket/websocket.service.ts b/client/src/services/websocket/websocket.service.ts index e9766e6..1f2c1cb 100644 --- a/client/src/services/websocket/websocket.service.ts +++ b/client/src/services/websocket/websocket.service.ts @@ -53,7 +53,7 @@ export class WebsocketService implements OnDestroy { this.ws.close(); } - this.ws = new WebSocket('ws://localhost:8080/ws'); + this.ws = new WebSocket('wss://localhost:10443/ws'); this.ws.onmessage = (event) => { this.messageHandler(WebsocketService.messageToObject(event.data)); diff --git a/etc/airshipui.json.example b/etc/airshipui.json.example new file mode 100755 index 0000000..20ad5ac --- /dev/null +++ b/etc/airshipui.json.example @@ -0,0 +1,15 @@ +{ + "webservice": { + "host": "", + "port": , + "publicKey": "/", + "privateKey": "/" + }, + "dashboards": [ + { + "name": "Dash", + "baseURL": "https://", + "path": "//" + } + ] +} \ No newline at end of file diff --git a/pkg/commands/root.go b/pkg/commands/root.go index bab5a1c..eae2f4f 100644 --- a/pkg/commands/root.go +++ b/pkg/commands/root.go @@ -17,7 +17,6 @@ package commands import ( "os" "os/signal" - "path/filepath" "syscall" "github.com/spf13/cobra" @@ -38,11 +37,22 @@ var rootCmd = &cobra.Command{ func init() { // Add a 'version' command, in addition to the '--version' option that is auto created rootCmd.AddCommand(newVersionCmd()) + + // Add the config file Flag + rootCmd.Flags().StringVarP( + &configs.UIConfigFile, + "conf", + "c", + "etc/airshipui.json", + "This will set the location of the conf file needed to start the UI", + ) + + // Add the logging level flag rootCmd.Flags().IntVar( &log.LogLevel, "loglevel", 6, - "This well set the log level, anything at or below that level will be viewed, all others suppressed\n"+ + "This will set the log level, anything at or below that level will be viewed, all others suppressed\n"+ " 6 -- Trace\n"+ " 5 -- Debug\n"+ " 4 -- Info\n"+ @@ -53,16 +63,9 @@ func init() { } func launch(cmd *cobra.Command, args []string) { - // set default config path - // TODO: do we want to make this a flag that can be passed in? - airshipUIConfigPath, err := getDefaultConfigPath() - if err != nil { - log.Errorf("Error setting config path %s", err) - } - // Read AirshipUI config file - if err := configs.SetUIConfig(airshipUIConfigPath); err != nil { - log.Errorf("config %s", err) + if err := configs.SetUIConfig(); err != nil { + log.Fatalf("config %s", err) } // start webservice and listen for the the ctl + c to exit @@ -83,12 +86,3 @@ func Execute() { os.Exit(1) } } - -func getDefaultConfigPath() (string, error) { - home, err := os.UserHomeDir() - if err != nil { - return "", err - } - - return filepath.FromSlash(home + "/.airship/airshipui.json"), nil -} diff --git a/pkg/configs/configs.go b/pkg/configs/configs.go index 9d50600..564a897 100644 --- a/pkg/configs/configs.go +++ b/pkg/configs/configs.go @@ -15,20 +15,28 @@ package configs import ( + "crypto/rsa" "encoding/json" "io/ioutil" "os" + "path" + "path/filepath" "opendev.org/airship/airshipctl/pkg/config" + "opendev.org/airship/airshipui/pkg/cryptography" + "opendev.org/airship/airshipui/pkg/log" ) // variables related to UI config var ( - UIConfig Config + UIConfig Config + UIConfigFile string + etcDir *string ) // Config basic structure to hold configuration params for Airship UI type Config struct { + WebService *WebService `json:"webservice,omitempty"` AuthMethod *AuthMethod `json:"authMethod,omitempty"` Dashboards []Dashboard `json:"dashboards,omitempty"` } @@ -40,6 +48,14 @@ type AuthMethod struct { URL string `json:"url,omitempty"` } +// WebService describes the things we need to know to start the web container +type WebService struct { + Host string `json:"host,omitempty"` + Port int `json:"port,omitempty"` + PublicKey string `json:"publicKey,omitempty"` + PrivateKey string `json:"privateKey,omitempty"` +} + // Dashboard structure type Dashboard struct { Name string `json:"name,omitempty"` @@ -111,11 +127,11 @@ type WsMessage struct { // SetUIConfig sets the UIConfig object with values obtained from // airshipui.json, located at 'filename' -// TODO: add watcher to the json file to reload conf on change -func SetUIConfig(filename string) error { - f, err := os.Open(filename) +// TODO: add watcher to the json file to reload conf on change (maybe not needed) +func SetUIConfig() error { + f, err := os.Open(UIConfigFile) if err != nil { - return err + return checkConfigs() } defer f.Close() @@ -129,5 +145,101 @@ func SetUIConfig(filename string) error { return err } + return checkConfigs() +} + +func checkConfigs() error { + if UIConfig.WebService == nil { + log.Debug("No UI config found, generating ssl keys & host & port info") + err := setEtcDir() + if err != nil { + return err + } + + privateKeyFile := filepath.Join(*etcDir, "key.pem") + publicKeyFile := filepath.Join(*etcDir, "cert.pem") + + err = writeTestSSL(privateKeyFile, publicKeyFile) + if err != nil { + return err + } + + UIConfig.WebService = &WebService{ + Host: "localhost", + Port: 10443, + PublicKey: publicKeyFile, + PrivateKey: privateKeyFile, + } + err = cryptography.TestCertValidity(publicKeyFile) + if err != nil { + return err + } + + bytes, err := json.Marshal(UIConfig) + if err != nil { + return err + } + err = ioutil.WriteFile(UIConfigFile, bytes, 0440) + if err != nil { + return err + } + } + return nil +} + +func writeTestSSL(privateKeyFile string, publicKeyFile string) error { + // get and write out private key + log.Warnf("Generating private key %s. DO NOT USE THIS FOR PRODUCTION", privateKeyFile) + privateKey, err := getAndWritePrivateKey(privateKeyFile) + if err != nil { + return err + } + + // get and write out public key + log.Warnf("Generating public key %s. DO NOT USE THIS FOR PRODUCTION", publicKeyFile) + err = getAndWritePublicKey(publicKeyFile, privateKey) + if err != nil { + return err + } + + return nil +} + +func getAndWritePrivateKey(fileName string) (*rsa.PrivateKey, error) { + privateKeyBytes, privateKey, err := cryptography.GeneratePrivateKey() + if err != nil { + return nil, err + } + err = ioutil.WriteFile(fileName, privateKeyBytes, 0600) + if err != nil { + return nil, err + } + return privateKey, nil +} + +func getAndWritePublicKey(fileName string, privateKey *rsa.PrivateKey) error { + publicKeyBytes, err := cryptography.GeneratePublicKey(privateKey) + if err != nil { + return err + } + err = ioutil.WriteFile(fileName, publicKeyBytes, 0600) + if err != nil { + return err + } + return nil +} + +func setEtcDir() error { + if etcDir == nil { + dir, err := filepath.Abs(filepath.Dir(os.Args[0])) + if err != nil { + return err + } + dir, err = filepath.Abs(filepath.Join(path.Dir(dir), "etc")) + if err != nil { + return err + } + etcDir = &dir + } return nil } diff --git a/pkg/configs/configs_test.go b/pkg/configs/configs_test.go deleted file mode 100644 index ffc01cd..0000000 --- a/pkg/configs/configs_test.go +++ /dev/null @@ -1,64 +0,0 @@ -/* - 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 - - https://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 configs - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const ( - fakeFile string = "/fake/config/path" - testFile string = "testdata/airshipui.json" - invalidTestFile string = "testdata/airshipui_invalid.json" -) - -// DummyDashboardsConfig returns an array of populated Dashboard structs -func dummyDashboardsConfig() []Dashboard { - return []Dashboard{ - { - Name: "dummy_dashboard", - BaseURL: "http://dummyhost", - Path: "fake/login/path", - }, - } -} - -func dummyAuthMethodConfig() *AuthMethod { - return &AuthMethod{ - URL: "http://fake.auth.method.com/auth", - } -} - -func TestSetUIConfig(t *testing.T) { - conf := Config{ - Dashboards: dummyDashboardsConfig(), - AuthMethod: dummyAuthMethodConfig(), - } - - err := SetUIConfig(testFile) - require.NoError(t, err) - - assert.Equal(t, conf, UIConfig) - - err = SetUIConfig(invalidTestFile) - require.Error(t, err) -} - -func TestFileNotFound(t *testing.T) { - err := SetUIConfig(fakeFile) - assert.Error(t, err) -} diff --git a/pkg/configs/testdata/airshipui.json b/pkg/configs/testdata/airshipui.json deleted file mode 100644 index eae4084..0000000 --- a/pkg/configs/testdata/airshipui.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "authMethod": { - "url": "http://fake.auth.method.com/auth" - }, - "dashboards": [ - { - "name": "dummy_dashboard", - "baseURL": "http://dummyhost", - "path": "fake/login/path" - } - ] -} diff --git a/pkg/configs/testdata/airshipui_invalid.json b/pkg/configs/testdata/airshipui_invalid.json deleted file mode 100644 index 9ed07f0..0000000 --- a/pkg/configs/testdata/airshipui_invalid.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "authMethod": { - "url": "http://fake.auth.method.com/auth" - }, - "dashboards": [], -} diff --git a/pkg/configs/testdata/config.yaml b/pkg/configs/testdata/config.yaml deleted file mode 100644 index 41f85a6..0000000 --- a/pkg/configs/testdata/config.yaml +++ /dev/null @@ -1,73 +0,0 @@ -apiVersion: airshipit.org/v1alpha1 -bootstrapInfo: - default: - builder: - networkConfigFileName: network-config - outputMetadataFileName: output-metadata.yaml - userDataFileName: user-data - container: - containerRuntime: docker - image: quay.io/airshipit/isogen:latest-debian_stable - volume: /srv/iso:/config - remoteDirect: - isoUrl: http://localhost:8099/debian-custom.iso - dummy_bootstrap_config: - builder: - networkConfigFileName: netconfig - outputMetadataFileName: output-metadata.yaml - userDataFileName: user-data - container: - containerRuntime: docker - image: dummy_image:dummy_tag - volume: /dummy:dummy -clusters: - kubernetes: - clusterType: - target: - bootstrapInfo: default - clusterKubeconf: kubernetes_target - managementConfiguration: default -contexts: - admin@kubernetes: - contextKubeconf: kubernetes_target -currentContext: admin@kubernetes -kind: Config -managementConfiguration: - default: - insecure: true - systemActionRetries: 30 - systemRebootDelay: 30 - type: redfish - dummy_management_config: - insecure: true - type: redfish -manifests: - default: - primaryRepositoryName: primary - repositories: - primary: - checkout: - branch: master - commitHash: "" - force: false - tag: "" - url: https://opendev.org/airship/treasuremap - subPath: treasuremap/manifests/site - targetPath: /tmp/default - dummy_manifest: - primaryRepositoryName: primary - repositories: - primary: - auth: - sshKey: testdata/test-key.pem - type: ssh-key - checkout: - branch: "" - commitHash: "" - force: false - tag: v1.0.1 - url: http://dummy.url.com/manifests.git - subPath: manifests/site/test-site - targetPath: /var/tmp/ -users: - admin: {} diff --git a/pkg/configs/testdata/kubeconfig.yaml b/pkg/configs/testdata/kubeconfig.yaml deleted file mode 100644 index 20428d8..0000000 --- a/pkg/configs/testdata/kubeconfig.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: v1 -clusters: -- cluster: - certificate-authority: pki/cluster-ca.pem - server: https://10.0.0.1:6553 - name: kubernetes_target -contexts: -- context: - cluster: kubernetes_target - user: admin - name: admin@kubernetes -current-context: admin@kubernetes -kind: Config -preferences: {} -users: -- name: admin - user: - client-certificate: pki/admin.pem - client-key: pki/admin-key.pem diff --git a/pkg/cryptography/cryptography.go b/pkg/cryptography/cryptography.go new file mode 100755 index 0000000..4903bcc --- /dev/null +++ b/pkg/cryptography/cryptography.go @@ -0,0 +1,110 @@ +/* + 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 + + https://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 cryptography + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "io/ioutil" + "math/big" + "time" + + "opendev.org/airship/airshipui/pkg/log" +) + +const ( + keySize = 4096 // 4k key +) + +// GeneratePrivateKey will a pem encoded private key and an rsa private key object +func GeneratePrivateKey() ([]byte, *rsa.PrivateKey, error) { + privateKey, err := rsa.GenerateKey(rand.Reader, keySize) + if err != nil { + log.Error("Problem generating private key", err) + return nil, nil, err + } + + buf := &bytes.Buffer{} + err = pem.Encode(buf, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(privateKey), + }) + if err != nil { + log.Error("Problem generating private key pem", err) + return nil, nil, err + } + + return buf.Bytes(), privateKey, nil +} + +// GeneratePublicKey will create a pem encoded cert +func GeneratePublicKey(privateKey *rsa.PrivateKey) ([]byte, error) { + template := generateCSR() + derCert, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) + if err != nil { + return nil, err + } + + buf := &bytes.Buffer{} + err = pem.Encode(buf, &pem.Block{ + Type: "CERTIFICATE", + Bytes: derCert, + }) + if err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// generateCSR creates the base information needed to create the certificate +func generateCSR() x509.Certificate { + return x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + CommonName: "localhost", + Organization: []string{"Airship UI"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(1, 0, 0), + BasicConstraintsValid: true, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + } +} + +// TestCertValidity will check if the cert defined in the conf is not past its not after date +func TestCertValidity(pemFile string) error { + r, err := ioutil.ReadFile(pemFile) + if err != nil { + log.Error(err) + return err + } + + block, _ := pem.Decode(r) + _, err = x509.ParseCertificate(block.Bytes) + if err != nil { + log.Error(err) + return err + } + + // calculate the validity of the cert + // TODO: Add a cert check for time based validity here + // fmt.Println(cert.NotAfter) + return nil +} diff --git a/pkg/webservice/server_test.go b/pkg/cryptography/cryptography_test.go old mode 100644 new mode 100755 similarity index 52% rename from pkg/webservice/server_test.go rename to pkg/cryptography/cryptography_test.go index 93eaab7..abd8010 --- a/pkg/webservice/server_test.go +++ b/pkg/cryptography/cryptography_test.go @@ -12,31 +12,35 @@ limitations under the License. */ -package webservice +package cryptography import ( - "net/http" "testing" - "time" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -const ( - serverAddr string = "localhost:8080" -) - -func init() { - go WebServer() - // wait for the webserver to come up - time.Sleep(250 * time.Millisecond) -} - -func TestRootURI(t *testing.T) { - resp, err := http.Get("http://" + serverAddr) +func TestGeneratePrivateKey(t *testing.T) { + pem, key, err := GeneratePrivateKey() require.NoError(t, err) - defer resp.Body.Close() - // this will be not found because of where the webservice starts - assert.Equal(t, http.StatusNotFound, resp.StatusCode) + require.NotNil(t, key) + require.NotNil(t, pem) +} + +func TestGeneratePublicKey(t *testing.T) { + _, privateKey, err := GeneratePrivateKey() + require.NoError(t, err) + + cert, err := GeneratePublicKey(privateKey) + require.NoError(t, err) + require.NotNil(t, cert) +} + +func TestTestCertValidity(t *testing.T) { + _, privateKey, err := GeneratePrivateKey() + require.NoError(t, err) + + cert, err := GeneratePublicKey(privateKey) + require.NoError(t, err) + require.NotNil(t, cert) } diff --git a/pkg/webservice/server.go b/pkg/webservice/server.go index 1c488f7..79ec641 100755 --- a/pkg/webservice/server.go +++ b/pkg/webservice/server.go @@ -16,6 +16,7 @@ package webservice import ( "net/http" + "strconv" "github.com/pkg/errors" "opendev.org/airship/airshipui/pkg/configs" @@ -85,7 +86,11 @@ func WebServer() { // start proxies for web based use startProxies() - // TODO: pull ports out into conf files - log.Info("Attempting to start webservice on localhost:8080") - log.Fatal(http.ListenAndServe(":8080", webServerMux)) + // Calculate the address and start on the host and port specified in the config + addr := configs.UIConfig.WebService.Host + ":" + strconv.Itoa(configs.UIConfig.WebService.Port) + log.Infof("Attempting to start webservice on %s", addr) + log.Fatal(http.ListenAndServeTLS(addr, + configs.UIConfig.WebService.PublicKey, + configs.UIConfig.WebService.PrivateKey, + webServerMux)) } diff --git a/pkg/webservice/websocket_test.go b/pkg/webservice/websocket_test.go deleted file mode 100644 index d0c53dc..0000000 --- a/pkg/webservice/websocket_test.go +++ /dev/null @@ -1,120 +0,0 @@ -/* - 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 - - https://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 webservice - -import ( - "encoding/json" - "net/url" - "testing" - "time" - - "github.com/gorilla/websocket" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "opendev.org/airship/airshipui/pkg/configs" - "opendev.org/airship/airshipui/pkg/log" -) - -const ( - // client messages - keepalive string = `{"type":"ui","component":"keepalive"}` - unknownType string = `{"type":"fake_type","component":"initialize"}` - unknownComponent string = `{"type":"ui","component":"fake_component"}` -) - -var client *websocket.Conn - -func init() { - u := url.URL{Scheme: "ws", Host: serverAddr, Path: "/ws"} - var err error - client, _, err = websocket.DefaultDialer.Dial(u.String(), nil) - if err != nil { - log.Fatal(err) - } - - time.Sleep(10 * time.Millisecond) - // get server response to "initialize" message from client which is sent by default - var response configs.WsMessage - err = client.ReadJSON(&response) - if err != nil { - log.Fatal(err) - } -} - -func TestKeepalive(t *testing.T) { - // get server response to "keepalive" message from client - response, err := getResponse(keepalive) - require.NoError(t, err) - - expected := configs.WsMessage{ - SessionID: response.SessionID, - Type: configs.UI, - Component: configs.Keepalive, - // don't fail on timestamp diff - Timestamp: response.Timestamp, - } - - assert.Equal(t, expected, response) -} - -func TestUnknownType(t *testing.T) { - response, err := getResponse(unknownType) - require.NoError(t, err) - - expected := configs.WsMessage{ - SessionID: response.SessionID, - Type: "fake_type", - Component: configs.Initialize, - // don't fail on timestamp diff - Timestamp: response.Timestamp, - Error: "Requested type: fake_type, not found", - } - - assert.Equal(t, expected, response) -} - -func TestUnknownComponent(t *testing.T) { - response, err := getResponse(unknownComponent) - require.NoError(t, err) - - expected := configs.WsMessage{ - SessionID: response.SessionID, - Type: configs.UI, - Component: "fake_component", - // don't fail on timestamp diff - Timestamp: response.Timestamp, - Error: "Requested component: fake_component, not found", - } - - assert.Equal(t, expected, response) -} - -func getResponse(message string) (configs.WsMessage, error) { - err := client.WriteJSON(json.RawMessage(message)) - - time.Sleep(50 * time.Millisecond) - - if err != nil { - return configs.WsMessage{}, err - } - - var response configs.WsMessage - err = client.ReadJSON(&response) - if err != nil { - return configs.WsMessage{}, err - } - - return response, nil -}