Skip to content

Commit

Permalink
Merge pull request #53 from zowe/feature/add-tls
Browse files Browse the repository at this point in the history
TLS FTP support
  • Loading branch information
1000TurquoisePogs authored Oct 3, 2023
2 parents 4e2e1ff + 63115f1 commit 0f06959
Show file tree
Hide file tree
Showing 9 changed files with 706 additions and 100 deletions.
624 changes: 538 additions & 86 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions src/actions/ConnectionHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ export class FTPConnection extends Connection {

saveConnectionData(args: IIpcConnectionArgs): IResponse {
const details = Object.keys(args).reduce((acc: string, k: keyof IIpcConnectionArgs) => {
const value = args[k].toString();

const value = (typeof args[k] == 'number') ? args[k].toString() : args[k];
const status = ConnectionStore.set(`ftp-details.${k}`, value);
return acc + status ? '' : `\n Can't set ftp-details.${k}, check the store schema`;
}, "");
Expand All @@ -68,4 +69,4 @@ export class CLIConnection extends Connection {
throw new Error('CLIConnection is not implemented');
}

}
}
6 changes: 5 additions & 1 deletion src/renderer/components/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,11 @@ const Home = () => {
window.electron.ipcRenderer.findPreviousInstallations().then((res: IResponse) => {
const connectionStore = res.details;
if (connectionStore["connection-type"] === 'ftp') {
const connectionArgs: IIpcConnectionArgs = {...connectionStore["ftp-details"], password: '', connectionType: 'ftp'};
console.log(JSON.stringify(connectionStore['ftp-details'],null,2));
const connectionArgs: IIpcConnectionArgs = {
...connectionStore["ftp-details"],
password: '',
connectionType: 'ftp'};
dispatch(setConnectionArgs(connectionArgs));
} else {
// TODO: Add support for other types
Expand Down
88 changes: 82 additions & 6 deletions src/renderer/components/stages/connection/Connection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,27 @@
* Copyright Contributors to the Zowe Project.
*/

import React, { SyntheticEvent, useEffect } from "react";
import React, { SyntheticEvent, useEffect, useState } from "react";
import Accordion from '@mui/material/Accordion';
import AccordionSummary from '@mui/material/AccordionSummary';
import AccordionDetails from '@mui/material/AccordionDetails';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import MenuItem from '@mui/material/MenuItem';
import Checkbox from '@mui/material/Checkbox';
import FormControl from '@mui/material/FormControl';
import FormControlLabel from '@mui/material/FormControlLabel';
import Typography from '@mui/material/Typography';
import secureIcon from '../../../assets/secure.png';
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined';
import ContainerCard from '../../common/ContainerCard';
import { useAppSelector, useAppDispatch } from '../../../hooks';
import { IResponse } from '../../../../types/interfaces';
import { setConnectionArgs, setConnectionStatus, selectConnectionArgs, selectConnectionStatus } from './connectionSlice';
import { setConnectionArgs, setConnectionStatus, selectConnectionArgs, selectConnectionStatus, setHost, setPort,
setUser, setPassword, setJobStatement, setSecure, setSecureOptions } from './connectionSlice';
import { setLoading, setNextStepEnabled, selectZoweCLIVersion } from '../../configuration-wizard/wizardSlice';
import { Container } from "@mui/material";
import { alertEmitter } from "../../Header";
Expand Down Expand Up @@ -123,7 +127,7 @@ const FTPConnectionForm = () => {
variant="standard"
helperText="Target system for Zowe z/OS components installation"
value={connectionArgs.host}
onChange={(e) => {dispatch(setConnectionArgs({...connectionArgs, host: e.target.value}))}}
onChange={(e) => { dispatch(setHost(e.target.value)) }}
/>
</FormControl>
<FormControl>
Expand All @@ -135,7 +139,7 @@ const FTPConnectionForm = () => {
variant="standard"
helperText="FTP port number. If you'll not specify we try use default service port"
value={connectionArgs.port}
onChange={(e) => dispatch(setConnectionArgs({...connectionArgs, port: Number(e.target.value)}))}
onChange={(e) => { dispatch(setPort(Number(e.target.value))) }}
/>
</FormControl>
<FormControl>
Expand All @@ -146,7 +150,7 @@ const FTPConnectionForm = () => {
variant="standard"
helperText="Your z/OS (Mainframe) user name"
value={connectionArgs.user}
onChange={(e) => dispatch(setConnectionArgs({...connectionArgs, user: e.target.value}))}
onChange={(e) => { dispatch(setUser(e.target.value)) }}
/>
</FormControl>
<FormControl>
Expand All @@ -162,9 +166,81 @@ const FTPConnectionForm = () => {
<span>We keep your password only for the current session</span>
</span>}
value={connectionArgs.password}
onChange={(e) => dispatch(setConnectionArgs({...connectionArgs, password: e.target.value}))}
onChange={(e) => { dispatch(setPassword(e.target.value)) }}
/>
</FormControl>
<FormControl>
<Container sx={{display: "flex", justifyContent: "center", flexDirection: "row"}}>
<FormControlLabel
control={<Checkbox
onChange={(e) => { dispatch(setSecure(e.target.checked)) }}
/>}
label="Use FTP over TLS"
labelPlacement="start"
value={connectionArgs.secure}
/>
</Container>
</FormControl>

{connectionArgs.secure &&
<Container sx={{
borderTopLeftRadius: "1rem", borderTopRightRadius: "1rem", borderBottomRightRadius: "1rem", borderBottomLeftRadius: "1rem",
borderColor: "#aaaaaa", backgroundColor: "#f0f0f0", borderStyle: "solid", padding: "1rem"
}}>
<FormControl>
<TextField
id="standard-required"
label="Min TLS"
variant="standard"
select={true}
helperText="Minimum TLS version to accept from server"
value={connectionArgs.secureOptions.minVersion}
onChange={(e) => { dispatch(setSecureOptions({...connectionArgs.secureOptions, minVersion: e.target.value})) }}

>
<MenuItem value={"TLSv1"}>1.0</MenuItem>
<MenuItem value={"TLSv1.1"}>1.1</MenuItem>
<MenuItem value={"TLSv1.2"}>1.2</MenuItem>
<MenuItem value={"TLSv1.3"}>1.3</MenuItem>
</TextField>
</FormControl>
<FormControl>
<TextField
id="standard-required"
label="Max TLS"
variant="standard"
select={true}
helperText="Maximum TLS version to accept from server"
value={connectionArgs.secureOptions.maxVersion}
onChange={(e) => { dispatch(setSecureOptions({...connectionArgs.secureOptions, maxVersion: e.target.value})) }}

>
<MenuItem value={"TLSv1"}>1.0</MenuItem>
<MenuItem value={"TLSv1.1"}>1.1</MenuItem>
<MenuItem value={"TLSv1.2"}>1.2</MenuItem>
<MenuItem value={"TLSv1.3"}>1.3</MenuItem>
</TextField>
</FormControl>

<FormControl>
<Container sx={{display: "flex", justifyContent: "left", flexDirection: "row"}}>
<FormControlLabel
control={<Checkbox
onChange={(e) => { dispatch(setSecureOptions({...connectionArgs.secureOptions, rejectUnauthorized: !e.target.value})) }}
/>}
label="Accept all certificates"
labelPlacement="start"
value={!connectionArgs.secureOptions.rejectUnauthorized}
/>
</Container>
</FormControl>

</Container>
}




<Container sx={{display: "flex", justifyContent: "center", flexDirection: "row"}}>
<Button sx={{boxShadow: 'none'}} type="submit" variant="text" onClick={() => processForm()}>Validate credentials</Button>
<div style={{opacity: formProcessed ? '1' : '0', minWidth: '32px', paddingLeft: '12px'}}>
Expand Down
39 changes: 36 additions & 3 deletions src/renderer/components/stages/connection/connectionSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@

import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { RootState } from '../../../store';
import { IIpcConnectionArgs } from '../../../../types/interfaces';
import { IIpcConnectionArgs, IIpcConnectionArgsSecureOptions } from '../../../../types/interfaces';

export interface ConnectionState {
connectionStatus: boolean;
connectionArgs: IIpcConnectionArgs;
}

//TODO also seen in ConnectionStore. Necessary or duplication?
const initialState: ConnectionState = {
connectionStatus: false,
connectionArgs: {
Expand All @@ -26,6 +27,16 @@ const initialState: ConnectionState = {
user: '',
password: '',
jobStatement: '',
secure: false,
secureOptions: {
enableTrace: false,
rejectUnauthorized: true,
//per node doc, if secureContext missing, this object will be used to create instead
//the content below is from the object meant to be passed to secureContext.
//TODO create a "MAX" and "MIN" that gets set to tls.DEFAULT_MAX/MIN_VERSION on server-side?
maxVersion: "TLSv1.3",
minVersion: "TLSv1.1"
}
},
};

Expand All @@ -36,14 +47,36 @@ export const connectionSlice = createSlice({
setConnectionArgs: (state, action: PayloadAction<IIpcConnectionArgs>) => {
state.connectionArgs = action.payload;
},
setHost: (state, action: PayloadAction<string>) => {
state.connectionArgs.host = action.payload;
},
setPort: (state, action: PayloadAction<number>) => {
state.connectionArgs.port = action.payload;
},
setUser: (state, action: PayloadAction<string>) => {
state.connectionArgs.user = action.payload;
},
setPassword: (state, action: PayloadAction<string>) => {
state.connectionArgs.password = action.payload;
},
setJobStatement: (state, action: PayloadAction<string>) => {
state.connectionArgs.jobStatement = action.payload;
},
setSecure: (state, action: PayloadAction<boolean>) => {
state.connectionArgs.secure = action.payload;
},
setSecureOptions: (state, action: PayloadAction<IIpcConnectionArgsSecureOptions>) => {
state.connectionArgs.secureOptions = action.payload;
},
setConnectionStatus: (state, action: PayloadAction<boolean>) => {
state.connectionStatus = action.payload;
},
},
});

// REVIEW: Split to distinct: setHost, setPort, setConnectionType, setUser, setPassword
export const { setConnectionArgs, setConnectionStatus } = connectionSlice.actions;
export const { setConnectionArgs, setConnectionStatus, setHost, setPort,
setUser, setPassword, setJobStatement, setSecure, setSecureOptions,
} = connectionSlice.actions;

export const selectConnectionArgs = (state: RootState) => state.connection.connectionArgs;
export const selectConnectionStatus = (state: RootState) => state.connection.connectionStatus;
Expand Down
3 changes: 2 additions & 1 deletion src/services/SubmitJcl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ export async function submitJcl(config: IIpcConnectionArgs, jcl: string, returnD
let client
try {
client = await connectFTPServer(config);
console.log('About to submit jcl=',jcl);
const jobId = await client.submitJCL(jcl);
console.log(`jobId: ${jobId}`);
jobOutput = await waitForjobToFinish(client, jobId, returnDDs)

client.close();

console.log(`job result=`,jobOutput);
resolve(jobOutput);

} catch (err) {
Expand Down
1 change: 1 addition & 0 deletions src/services/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const zos = require('zos-node-accessor');
export async function connectFTPServer(config: IIpcConnectionArgs): Promise<any> {

const client = new zos();
//Config Doc: https://github.com/IBM/zos-node-accessor/blob/3e32cc8b8cbd74c6bc3c29c268338e23f173bebc/README.md?plain=1#L89
await client.connect(config);
if (!client.connected) {
console.error('Failed to connect to', config.host);
Expand Down
29 changes: 28 additions & 1 deletion src/storage/ConnectionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,26 @@ const storeSchema = {
},
"jobStatement": {
"type": "string"
},
"secure": {
"type": "boolean"
},
"secureOptions": {
"type": "object",
"properties": {
"enableTrace": {
"type": "boolean",
},
"rejectUnauthorized": {
"type": "boolean"
},
"minVersion": {
"type": "string"
},
"maxVersion": {
"type": "string"
}
}
}
},
},
Expand All @@ -53,6 +73,13 @@ const storeDefault = {
"host": "",
"port": "21",
"user": "",
"secure": false,
"secureOptions": {
"enableTrace": false,
"rejectUnauthorized": true,
"maxVersion": "TLSv1.3",
"minVersion": "TLSv1.1"
},
"jobStatement": `//ZWEJOB01 JOB IZUACCT,'SYSPROG',CLASS=A,
// MSGLEVEL=(1,1),MSGCLASS=A`
},
Expand Down Expand Up @@ -86,7 +113,7 @@ export class ConnectionStore {
return store.store;
}

public static set(key: string, value: string): boolean {
public static set(key: string, value: any): boolean {
if (validateWithSchema(key)) {
store.set(key, value);
return true;
Expand Down
11 changes: 11 additions & 0 deletions src/types/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,24 @@ export type JobOutput = {
[key: string]: string;
};

export interface IIpcConnectionArgsSecureOptions {
enableTrace: boolean;
rejectUnauthorized: boolean;
//per node doc, if secureContext missing, this object will be used to create instead
//the content below is from the object meant to be passed to secureContext.
maxVersion: string;
minVersion: string;
}

export interface IIpcConnectionArgs {
host: string;
port?: number;
connectionType?: 'ftp' | 'sftp' | 'zosmf';
user: string;
password: string;
jobStatement: string;
secure: boolean;
secureOptions: IIpcConnectionArgsSecureOptions;
}

// TODO: Add some structure to res.details to highlight proper input field
Expand Down

0 comments on commit 0f06959

Please sign in to comment.