From 2eac6fbd3a18b8d116c8dacd8961bee1cadd90ba Mon Sep 17 00:00:00 2001 From: Yann Stepienik Date: Tue, 4 Apr 2023 21:54:35 +0100 Subject: [PATCH] v0.1.9 Various bug fixes and UI improvements to route form --- .../pages/config/users/containerPicker.jsx | 75 +++++++++++-------- .../src/pages/config/users/formShortcuts.jsx | 12 +-- client/src/pages/config/users/proxyman.jsx | 45 +++++++++-- client/src/pages/config/users/routeman.jsx | 45 +++++++++-- package.json | 2 +- readme.md | 2 +- src/proxy/routerGen.go | 36 ++++----- src/utils/middleware.go | 2 + 8 files changed, 148 insertions(+), 71 deletions(-) diff --git a/client/src/pages/config/users/containerPicker.jsx b/client/src/pages/config/users/containerPicker.jsx index c76c721..6652acb 100644 --- a/client/src/pages/config/users/containerPicker.jsx +++ b/client/src/pages/config/users/containerPicker.jsx @@ -36,9 +36,9 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer}) { const [containers, setContainers] = React.useState([]); const [hasPublicPorts, setHasPublicPorts] = React.useState(false); const [isOnBridge, setIsOnBridge] = React.useState(false); - const [options, setOptions] = React.useState([]); + const [options, setOptions] = React.useState(null); const [portsOptions, setPortsOptions] = React.useState([]); - const loading = open && options.length === 0; + const loading = options === null; const name = "Target" const label = "Container Name" @@ -50,27 +50,23 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer}) { let preview = formik.values[name]; - if(preview && preview.includes("://") && preview.includes(":")) { - let p1_ = preview.split("://")[1] + if(preview) { + let protocols = preview.split("://") + let p1_ = protocols.length > 1 ? protocols[1] : protocols[0] + console.log("p1_", p1_) targetResult = { container: '/' + p1_.split(":")[0], port: p1_.split(":")[1], protocol: preview.split("://")[0], - containerObject: containers.find((container) => container.Names[0] === '/' + p1_.split(":")[0]), + containerObject: !loading && containers.find((container) => container.Names[0] === '/' + p1_.split(":")[0]), } } function getTarget() { - return targetResult.protocol + "://" + targetResult.container.replace("/", "") + ":" + targetResult.port + return targetResult.protocol + (targetResult.protocol != '' ? "://" : '') + targetResult.container.replace("/", "") + ":" + targetResult.port } - const onContainerChange = (newContainer) => { - targetResult.container = newContainer.Names[0] - targetResult.containerObject = newContainer - targetResult.port = '' - targetResult.protocol = 'http' - formik.setFieldValue(name, getTarget()) - + const postContainerChange = (newContainer) => { let portsTemp = [] newContainer.Ports.forEach((port) => { portsTemp.push(port.PrivatePort) @@ -85,6 +81,17 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer}) { } } + const onContainerChange = (newContainer) => { + if(loading) return; + targetResult.container = newContainer.Names[0] + targetResult.containerObject = newContainer + targetResult.port = '' + targetResult.protocol = 'http' + formik.setFieldValue(name, getTarget()) + + postContainerChange(newContainer) + } + React.useEffect(() => { if(lockTarget) { onContainerChange(TargetContainer) @@ -93,7 +100,7 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer}) { React.useEffect(() => { let active = true; - + if (!loading) { return undefined; } @@ -101,13 +108,15 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer}) { (async () => { const res = await API.docker.list() setContainers(res.data); - let names = res.data.map((container) => container.Names[0]) if (active) { setOptions([...names]); } + if (targetResult.container !== 'null') { + postContainerChange(res.data.find((container) => container.Names[0] === targetResult.container)) + } })(); return () => { @@ -124,7 +133,7 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer}) { return ( {label} - ( )} - /> - + />} + {(portsOptions.length > 0) ? (<> Container Port { @@ -184,24 +193,24 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer}) { {option} ))} - ) : ''} + + {targetResult.port == '' && + Please select a port + } + ) : ''} {(portsOptions.length > 0) ? (<> - - - Container Protocol (use HTTP if unsure) + } + defaultValue={targetResult.protocol} onChange={(event) => { - targetResult.protocol = event.target.checked ? "https" : "http" + targetResult.protocol = event.target.value && event.target.value.toLowerCase() formik.setFieldValue(name, getTarget()) }} - label={"Container uses HTTPS internally (leave unchecked if not sure, they usually don't)"} /> - - ) : ''} Result Target Preview @@ -212,6 +221,12 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer}) { value={formik.values[name]} disabled={true} /> + + {formik.errors[name] && ( + + {formik.errors[name]} + + )} ); diff --git a/client/src/pages/config/users/formShortcuts.jsx b/client/src/pages/config/users/formShortcuts.jsx index 1a95965..6a1ed9a 100644 --- a/client/src/pages/config/users/formShortcuts.jsx +++ b/client/src/pages/config/users/formShortcuts.jsx @@ -38,12 +38,7 @@ export const CosmosInputText = ({ name, style, multiline, type, placeholder, onC name={name} multiline={multiline} onBlur={formik.handleBlur} - onChange={(...e) => { - if (onChange) { - onChange(...e); - } - formik.handleChange(...e); - }} + onChange={formik.handleChange} placeholder={placeholder} fullWidth error={Boolean(formik.touched[name] && formik.errors[name])} @@ -172,6 +167,11 @@ export const CosmosCheckbox = ({ name, label, formik, style }) => { style={style} /> + {formik.touched[name] && formik.errors[name] && ( + + {formik.errors[name]} + + )} } diff --git a/client/src/pages/config/users/proxyman.jsx b/client/src/pages/config/users/proxyman.jsx index fe4ccde..e857b74 100644 --- a/client/src/pages/config/users/proxyman.jsx +++ b/client/src/pages/config/users/proxyman.jsx @@ -28,7 +28,8 @@ import { import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons'; import AnimateButton from '../../../components/@extended/AnimateButton'; import RestartModal from './restart'; -import RouteManagement from './routeman'; +import RouteManagement, {ValidateRoute} from './routeman'; +import { map } from 'lodash'; const ProxyManagement = () => { @@ -36,6 +37,7 @@ const ProxyManagement = () => { const [config, setConfig] = React.useState(null); const [openModal, setOpenModal] = React.useState(false); const [error, setError] = React.useState(null); + const [submitErrors, setSubmitErrors] = React.useState([]); function updateRoutes(routes) { let con = { @@ -52,6 +54,14 @@ const ProxyManagement = () => { return con; } + function cleanRoutes(config) { + config.HTTPConfig.ProxyConfig.Routes = config.HTTPConfig.ProxyConfig.Routes.map((r) => { + delete r._hasErrors; + return r; + }); + return config; + } + function refresh() { API.config.get().then((res) => { setConfig(res.data); @@ -85,6 +95,13 @@ const ProxyManagement = () => { refresh(); }, []); + const testRoute = (route) => { + try { + ValidateRoute.validateSync(route); + } catch (e) { + return e.errors; + } + } let routes = config && (config.HTTPConfig.ProxyConfig.Routes || []); return
@@ -92,7 +109,7 @@ const ProxyManagement = () => { refresh(); }}>Refresh   + } diff --git a/client/src/pages/config/users/routeman.jsx b/client/src/pages/config/users/routeman.jsx index 4f0e656..6d8fc3a 100644 --- a/client/src/pages/config/users/routeman.jsx +++ b/client/src/pages/config/users/routeman.jsx @@ -33,9 +33,35 @@ import { CosmosCheckbox, CosmosCollapse, CosmosFormDivider, CosmosInputText, Cos import { DownOutlined, UpOutlined, CheckOutlined, DeleteOutlined } from '@ant-design/icons'; import { CosmosContainerPicker } from './containerPicker'; +export const ValidateRoute = Yup.object().shape({ + Name: Yup.string().required('Name is required'), + Mode: Yup.string().required('Mode is required'), + Target: Yup.string().required('Target is required').when('Mode', { + is: 'SERVAPP', + then: Yup.string().matches(/:[0-9]+$/, 'Invalid Target, must have a port'), + }), + + Host: Yup.string().when('UseHost', { + is: true, + then: Yup.string().required('Host is required') + .matches(/[\.|\:]/, 'Host must be full domain ([sub.]domain.com) or an IP') + }), + + PathPrefix: Yup.string().when('UsePathPrefix', { + is: true, + then: Yup.string().required('Path Prefix is required').matches(/^\//, 'Path Prefix must start with / (e.g. /api). Do not include a domain/subdomain in it, use the Host for this.') + }), + + UseHost: Yup.boolean().when('UsePathPrefix', + { + is: false, + then: Yup.boolean().oneOf([true], 'Source must at least be either Host or Path Prefix') + }), +}) + const RouteManagement = ({ routeConfig, TargetContainer, noControls=false, lockTarget=false, setRouteConfig, up, down, deleteRoute }) => { const [confirmDelete, setConfirmDelete] = React.useState(false); - + return
{routeConfig && <> { - setRouteConfig(values); - }} + validateOnChange={false} + validationSchema={ValidateRoute} onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => { return false; }} + validate={(values) => { + //setRouteConfig(values); + }} > {(formik) => (
@@ -76,6 +101,11 @@ const RouteManagement = ({ routeConfig, TargetContainer, noControls=false, lockT
}> + {formik.errors.submit && ( + + {formik.errors.submit} + + )} diff --git a/package.json b/package.json index 7de3b9c..6e5fe8a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cosmos-server", - "version": "0.1.8", + "version": "0.1.9", "description": "", "main": "test-server.js", "bugs": { diff --git a/readme.md b/readme.md index f1be7e5..52dc28c 100644 --- a/readme.md +++ b/readme.md @@ -70,7 +70,7 @@ Authentication is very hard (how do you check the password match? What encryptio Installation is simple using Docker: ``` -docker run -d -p 80:80 -p 443:443 --name cosmos-server --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v /path/to/cosmos/config:/config azukaar/cosmos-server:latest +docker run -d -p 80:80 -p 443:443 --name cosmos-server -h cosmos-server --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v /path/to/cosmos/config:/config azukaar/cosmos-server:latest ``` Once installed, simply go to `http://your-ip` and follow the instructions of the setup wizard. diff --git a/src/proxy/routerGen.go b/src/proxy/routerGen.go index 26d4870..ad469fa 100644 --- a/src/proxy/routerGen.go +++ b/src/proxy/routerGen.go @@ -63,21 +63,6 @@ func RouterGen(route utils.ProxyRouteConfig, router *mux.Router, destination htt } destination = http.StripPrefix(route.PathPrefix, destination) } - timeout := route.Timeout - - if(timeout == 0) { - timeout = 10000 - } - - throttlePerMinute := route.ThrottlePerMinute - - throtthleTime := 1*time.Minute - - // lets do something better later to disable throttle - if(throttlePerMinute == 0) { - throttlePerMinute = 99999999 - throtthleTime = 1*time.Second - } originCORS := route.CORSOrigin @@ -92,12 +77,18 @@ func RouterGen(route utils.ProxyRouteConfig, router *mux.Router, destination htt if(route.UsePathPrefix && !route.StripPathPrefix && (route.Mode == "STATIC" || route.Mode == "SPA")) { utils.Warn("PathPrefix is used, but StripPathPrefix is false. The route mode is " + (string)(route.Mode) + ". This will likely cause issues with the route. Ignore this warning if you know what you are doing.") } + + timeout := route.Timeout + + if(timeout > 0) { + destination = utils.MiddlewareTimeout(timeout * time.Millisecond)(destination) + } - origin.Handler( - tokenMiddleware(route.AuthEnabled)( - utils.CORSHeader(originCORS)( - utils.MiddlewareTimeout(timeout * time.Millisecond)( - httprate.Limit(throttlePerMinute, throtthleTime, + throttlePerMinute := route.ThrottlePerMinute + + if(throttlePerMinute > 0) { + throtthleTime := time.Minute + destination = httprate.Limit(throttlePerMinute, throtthleTime, httprate.WithKeyFuncs(httprate.KeyByIP), httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) { utils.Error("Too many requests. Throttling", nil) @@ -105,7 +96,10 @@ func RouterGen(route utils.ProxyRouteConfig, router *mux.Router, destination htt http.StatusTooManyRequests, "HTTP003") return }), - )(destination))))) + )(destination) + } + + origin.Handler(tokenMiddleware(route.AuthEnabled)(utils.CORSHeader(originCORS)((destination)))) utils.Log("Added route: ["+ (string)(route.Mode) + "] " + route.Host + route.PathPrefix + " to " + route.Target + "") diff --git a/src/utils/middleware.go b/src/utils/middleware.go index 551f7b0..ac3e614 100644 --- a/src/utils/middleware.go +++ b/src/utils/middleware.go @@ -22,6 +22,8 @@ func MiddlewareTimeout(timeout time.Duration) func(next http.Handler) http.Handl } }() + w.Header().Set("X-Timeout-Duration", timeout.String()) + r = r.WithContext(ctx) next.ServeHTTP(w, r) }