From 00c51199914c29620e5b0d0746131b4ca401b8c3 Mon Sep 17 00:00:00 2001 From: hhftechnologies Date: Mon, 14 Apr 2025 13:56:16 +0530 Subject: [PATCH 1/6] update --- api/handlers.go | 1 + config/templates.yaml | 36 ++++++- services/config_generator.go | 16 ++++ ui/src/App.js | 175 +++++++++++++++++++++++++++++++++-- 4 files changed, 217 insertions(+), 11 deletions(-) diff --git a/api/handlers.go b/api/handlers.go index c3be87311..96d991b22 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -497,6 +497,7 @@ func isValidMiddlewareType(typ string) bool { "addPrefix": true, "redirectRegex": true, "redirectScheme": true, + "chain": true, // Added support for chain middleware type } return validTypes[typ] diff --git a/config/templates.yaml b/config/templates.yaml index 9bbe0e3fd..7522cbbd6 100644 --- a/config/templates.yaml +++ b/config/templates.yaml @@ -57,4 +57,38 @@ middlewares: type: rateLimit config: average: 100 - burst: 50 \ No newline at end of file + burst: 50 + + - id: security-chain + name: Security Chain + type: chain + config: + middlewares: + - rate-limit + - ip-whitelist + + - id: headers-standard + name: Standard Security Headers + type: headers + config: + accessControlAllowMethods: + - GET + - OPTIONS + - PUT + browserXssFilter: true + contentTypeNosniff: true + customFrameOptionsValue: SAMEORIGIN + customResponseHeaders: + X-Forwarded-Proto: https + X-Robots-Tag: none,noarchive,nosnippet,notranslate,noimageindex + server: "" + forceSTSHeader: true + hostsProxyHeaders: + - X-Forwarded-Host + permissionsPolicy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), vr=() + referrerPolicy: same-origin + sslProxyHeaders: + X-Forwarded-Proto: https + stsIncludeSubdomains: true + stsPreload: true + stsSeconds: 63072000 \ No newline at end of file diff --git a/services/config_generator.go b/services/config_generator.go index ff9903543..3204812ce 100644 --- a/services/config_generator.go +++ b/services/config_generator.go @@ -140,6 +140,22 @@ func (cg *ConfigGenerator) processMiddlewares(config *TraefikConfig) error { continue } + // Special handling for chain middlewares to ensure proper provider prefixes + if typ == "chain" && middlewareConfig["middlewares"] != nil { + if middlewares, ok := middlewareConfig["middlewares"].([]interface{}); ok { + for i, middleware := range middlewares { + if middlewareStr, ok := middleware.(string); ok { + // If this is not already a fully qualified middleware reference + if !strings.Contains(middlewareStr, "@") { + // Assume it's from our file provider + middlewares[i] = fmt.Sprintf("%s@file", middlewareStr) + } + } + } + middlewareConfig["middlewares"] = middlewares + } + } + // Add middleware to config config.HTTP.Middlewares[id] = map[string]interface{}{ typ: middlewareConfig, diff --git a/ui/src/App.js b/ui/src/App.js index d2933fd00..2fd44059b 100644 --- a/ui/src/App.js +++ b/ui/src/App.js @@ -46,6 +46,53 @@ const parseMiddlewares = (middlewaresStr) => { }); }; +// Helper function to format middleware chains for display +const formatMiddlewareDisplay = (middleware, allMiddlewares) => { + // Check if this is a chain middleware + const isChain = middleware.type === 'chain'; + + // Get the config object + let configObj = middleware.config; + if (typeof configObj === 'string') { + try { + configObj = JSON.parse(configObj); + } catch (e) { + console.error('Error parsing middleware config:', e); + configObj = {}; + } + } + + return ( +
+
+ {middleware.name} + + {middleware.type} + + {isChain && (Middleware Chain)} +
+ + {/* Display chained middlewares if this is a chain */} + {isChain && configObj.middlewares && configObj.middlewares.length > 0 && ( +
+
Chain contains:
+
    + {configObj.middlewares.map((id, index) => { + const chainedMiddleware = allMiddlewares.find(m => m.id === id); + return ( +
  • + {index + 1}. {chainedMiddleware + ? {chainedMiddleware.name} ({chainedMiddleware.type}) + : {id} (unknown middleware)} +
  • + ); + })} +
+
+ )} +
+ ); +}; // Main App Component const App = () => { const [page, setPage] = useState('dashboard'); @@ -263,7 +310,6 @@ const Dashboard = ({ navigateTo }) => { ); }; - // Resources List Component const ResourcesList = ({ navigateTo }) => { const [resources, setResources] = useState([]); @@ -551,19 +597,24 @@ const ResourceDetail = ({ id, navigateTo }) => { - - + {assignedMiddlewares.map(middleware => { - const middlewareDetails = middlewares.find(m => m.id === middleware.id); + const middlewareDetails = middlewares.find(m => m.id === middleware.id) || { + id: middleware.id, + name: middleware.name, + type: 'unknown' + }; + return ( - - + - + + ); @@ -301,7 +343,25 @@ const Dashboard = ({ navigateTo }) => {

- You have {unprotectedResources} resources that are not protected with any middleware. + You have {unprotectedResources} active resources that are not protected with any middleware. +

+
+ + + )} + + {/* Warning for disabled resources */} + {disabledResources > 0 && ( +
+
+
+ + + +
+
+

+ You have {disabledResources} disabled resources that were removed from Pangolin. navigateTo('resources')}>View all resources to delete them.

@@ -310,6 +370,7 @@ const Dashboard = ({ navigateTo }) => {
); }; + // Resources List Component const ResourcesList = ({ navigateTo }) => { const [resources, setResources] = useState([]); @@ -335,6 +396,22 @@ const ResourcesList = ({ navigateTo }) => { fetchResources(); }, []); + const handleDeleteResource = async (id, host) => { + // eslint-disable-next-line no-restricted-globals + if (!confirm(`Are you sure you want to delete the resource "${host}"? This cannot be undone.`)) { + return; + } + + try { + await api.deleteResource(id); + alert('Resource deleted successfully'); + fetchResources(); + } catch (err) { + alert(`Failed to delete resource: ${err.message || 'Unknown error'}`); + console.error(err); + } + }; + const filteredResources = resources.filter(resource => resource.host.toLowerCase().includes(searchTerm.toLowerCase()) ); @@ -381,23 +458,42 @@ const ResourcesList = ({ navigateTo }) => { {filteredResources.map(resource => { const middlewaresList = parseMiddlewares(resource.middlewares); const isProtected = middlewaresList.length > 0; + const isDisabled = resource.status === 'disabled'; return ( - - + + - ); @@ -526,6 +622,8 @@ const ResourceDetail = ({ id, navigateTo }) => { ); } + const isDisabled = resource.status === 'disabled'; + return (
@@ -536,8 +634,57 @@ const ResourceDetail = ({ id, navigateTo }) => { Back

Resource: {resource.host}

+ {isDisabled && ( + + Removed from Pangolin + + )}
+ {/* Warning for disabled resources */} + {isDisabled && ( +
+
+
+ + + +
+
+

+ This resource has been removed from Pangolin and is now disabled. Any changes to middleware will not take effect. +

+
+ + +
+
+
+
+ )} + {/* Resource details */}

Resource Details

@@ -563,8 +710,11 @@ const ResourceDetail = ({ id, navigateTo }) => {

Status

- 0 ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'}`}> - {assignedMiddlewares.length > 0 ? 'Protected' : 'Not Protected'} + 0 ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800' + }`}> + {isDisabled ? 'Disabled' : assignedMiddlewares.length > 0 ? 'Protected' : 'Not Protected'}

@@ -581,7 +731,7 @@ const ResourceDetail = ({ id, navigateTo }) => {

Attached Middlewares

@@ -720,6 +871,7 @@ const ResourceDetail = ({ id, navigateTo }) => {
); }; + // Middlewares List Component const MiddlewaresList = ({ navigateTo }) => { const [middlewares, setMiddlewares] = useState([]); @@ -1194,4 +1346,5 @@ const MiddlewareForm = ({ id, isEditing, navigateTo }) => {
); }; + export default App; \ No newline at end of file From 3693aed23c32ef8c41763cb160346aa9ed2dfe7e Mon Sep 17 00:00:00 2001 From: hhftechnologies Date: Mon, 14 Apr 2025 17:36:47 +0530 Subject: [PATCH 5/6] update --- api/handlers.go | 110 ++++++++++++++++++++++++------------------------ api/routes.go | 5 ++- 2 files changed, 59 insertions(+), 56 deletions(-) diff --git a/api/handlers.go b/api/handlers.go index 3b203eb49..6a9ce4095 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -504,62 +504,64 @@ func (s *Server) assignMiddleware(c *gin.Context) { }) } +// removeMiddleware removes a middleware from a resource // removeMiddleware removes a middleware from a resource func (s *Server) removeMiddleware(c *gin.Context) { - resourceID := c.Param("resourceId") - middlewareID := c.Param("middlewareId") - - if resourceID == "" || middlewareID == "" { - ResponseWithError(c, http.StatusBadRequest, "Resource ID and Middleware ID are required") - return - } - - // Delete the relationship using a transaction - tx, err := s.db.Begin() - if err != nil { - log.Printf("Error beginning transaction: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - // If something goes wrong, rollback - defer func() { - if err != nil { - tx.Rollback() - } - }() - - result, err := tx.Exec( - "DELETE FROM resource_middlewares WHERE resource_id = ? AND middleware_id = ?", - resourceID, middlewareID, - ) - - if err != nil { - log.Printf("Error removing middleware: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Failed to remove middleware") - return - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - log.Printf("Error getting rows affected: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - if rowsAffected == 0 { - ResponseWithError(c, http.StatusNotFound, "Resource middleware relationship not found") - return - } - - // Commit the transaction - if err = tx.Commit(); err != nil { - log.Printf("Error committing transaction: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - c.JSON(http.StatusOK, gin.H{"message": "Middleware removed from resource successfully"}) + // Updated to use "id" parameter instead of "resourceId" to match route definition + resourceID := c.Param("id") + middlewareID := c.Param("middlewareId") + + if resourceID == "" || middlewareID == "" { + ResponseWithError(c, http.StatusBadRequest, "Resource ID and Middleware ID are required") + return + } + + // Delete the relationship using a transaction + tx, err := s.db.Begin() + if err != nil { + log.Printf("Error beginning transaction: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + // If something goes wrong, rollback + defer func() { + if err != nil { + tx.Rollback() + } + }() + + result, err := tx.Exec( + "DELETE FROM resource_middlewares WHERE resource_id = ? AND middleware_id = ?", + resourceID, middlewareID, + ) + + if err != nil { + log.Printf("Error removing middleware: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Failed to remove middleware") + return + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + log.Printf("Error getting rows affected: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + if rowsAffected == 0 { + ResponseWithError(c, http.StatusNotFound, "Resource middleware relationship not found") + return + } + + // Commit the transaction + if err = tx.Commit(); err != nil { + log.Printf("Error committing transaction: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Middleware removed from resource successfully"}) } // generateID generates a random 16-character hex string diff --git a/api/routes.go b/api/routes.go index b20d47d12..9f0e5872a 100644 --- a/api/routes.go +++ b/api/routes.go @@ -114,9 +114,10 @@ func (s *Server) setupRoutes(uiPath string) { { resources.GET("", s.getResources) resources.GET("/:id", s.getResource) - resources.DELETE("/:id", s.deleteResource) // New endpoint for deleting resources + resources.DELETE("/:id", s.deleteResource) // Delete resource endpoint resources.POST("/:id/middlewares", s.assignMiddleware) - resources.DELETE("/:resourceId/middlewares/:middlewareId", s.removeMiddleware) + // Fixed route parameter to use :id instead of :resourceId for consistency + resources.DELETE("/:id/middlewares/:middlewareId", s.removeMiddleware) } } From 950cb7c88a629a36808f8db7d30f9ead48f2f309 Mon Sep 17 00:00:00 2001 From: hhftechnologies Date: Mon, 14 Apr 2025 18:27:36 +0530 Subject: [PATCH 6/6] update --- api/handlers.go | 139 ++++++++++++++++++++++++++++++++++++++---- api/routes.go | 4 +- config/templates.yaml | 9 ++- ui/src/App.js | 90 +++++++++++++++++++++++---- 4 files changed, 214 insertions(+), 28 deletions(-) diff --git a/api/handlers.go b/api/handlers.go index 6a9ce4095..ffea94318 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -504,10 +504,124 @@ func (s *Server) assignMiddleware(c *gin.Context) { }) } -// removeMiddleware removes a middleware from a resource +// assignMultipleMiddlewares assigns multiple middlewares to a resource in one operation +func (s *Server) assignMultipleMiddlewares(c *gin.Context) { + resourceID := c.Param("id") + if resourceID == "" { + ResponseWithError(c, http.StatusBadRequest, "Resource ID is required") + return + } + + var input struct { + Middlewares []struct { + MiddlewareID string `json:"middleware_id" binding:"required"` + Priority int `json:"priority"` + } `json:"middlewares" binding:"required"` + } + + if err := c.ShouldBindJSON(&input); err != nil { + ResponseWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err)) + return + } + + // Verify resource exists and is active + var exists int + var status string + err := s.db.QueryRow("SELECT 1, status FROM resources WHERE id = ?", resourceID).Scan(&exists, &status) + if err == sql.ErrNoRows { + ResponseWithError(c, http.StatusNotFound, "Resource not found") + return + } else if err != nil { + log.Printf("Error checking resource existence: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + // Don't allow attaching middlewares to disabled resources + if status == "disabled" { + ResponseWithError(c, http.StatusBadRequest, "Cannot assign middlewares to a disabled resource") + return + } + + // Start a transaction + tx, err := s.db.Begin() + if err != nil { + log.Printf("Error beginning transaction: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + // If something goes wrong, rollback + defer func() { + if err != nil { + tx.Rollback() + } + }() + + // Process each middleware + successful := make([]map[string]interface{}, 0) + for _, mw := range input.Middlewares { + // Default priority is 100 if not specified + if mw.Priority <= 0 { + mw.Priority = 100 + } + + // Verify middleware exists + var middlewareExists int + err := s.db.QueryRow("SELECT 1 FROM middlewares WHERE id = ?", mw.MiddlewareID).Scan(&middlewareExists) + if err == sql.ErrNoRows { + // Skip this middleware but don't fail the entire request + log.Printf("Middleware %s not found, skipping", mw.MiddlewareID) + continue + } else if err != nil { + log.Printf("Error checking middleware existence: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + // First delete any existing relationship + _, err = tx.Exec( + "DELETE FROM resource_middlewares WHERE resource_id = ? AND middleware_id = ?", + resourceID, mw.MiddlewareID, + ) + if err != nil { + log.Printf("Error removing existing relationship: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + // Then insert the new relationship + _, err = tx.Exec( + "INSERT INTO resource_middlewares (resource_id, middleware_id, priority) VALUES (?, ?, ?)", + resourceID, mw.MiddlewareID, mw.Priority, + ) + if err != nil { + log.Printf("Error assigning middleware: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Failed to assign middleware") + return + } + + successful = append(successful, map[string]interface{}{ + "middleware_id": mw.MiddlewareID, + "priority": mw.Priority, + }) + } + + // Commit the transaction + if err = tx.Commit(); err != nil { + log.Printf("Error committing transaction: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + c.JSON(http.StatusOK, gin.H{ + "resource_id": resourceID, + "middlewares": successful, + }) +} + // removeMiddleware removes a middleware from a resource func (s *Server) removeMiddleware(c *gin.Context) { - // Updated to use "id" parameter instead of "resourceId" to match route definition resourceID := c.Param("id") middlewareID := c.Param("middlewareId") @@ -576,16 +690,17 @@ func generateID() (string, error) { // isValidMiddlewareType checks if a middleware type is valid func isValidMiddlewareType(typ string) bool { validTypes := map[string]bool{ - "basicAuth": true, - "forwardAuth": true, - "ipWhiteList": true, - "rateLimit": true, - "headers": true, - "stripPrefix": true, - "addPrefix": true, - "redirectRegex": true, - "redirectScheme": true, - "chain": true, + "basicAuth": true, + "forwardAuth": true, + "ipWhiteList": true, + "rateLimit": true, + "headers": true, + "stripPrefix": true, + "addPrefix": true, + "redirectRegex": true, + "redirectScheme": true, + "chain": true, + "replacepathregex": true, } return validTypes[typ] diff --git a/api/routes.go b/api/routes.go index 9f0e5872a..67f658112 100644 --- a/api/routes.go +++ b/api/routes.go @@ -114,9 +114,9 @@ func (s *Server) setupRoutes(uiPath string) { { resources.GET("", s.getResources) resources.GET("/:id", s.getResource) - resources.DELETE("/:id", s.deleteResource) // Delete resource endpoint + resources.DELETE("/:id", s.deleteResource) resources.POST("/:id/middlewares", s.assignMiddleware) - // Fixed route parameter to use :id instead of :resourceId for consistency + resources.POST("/:id/middlewares/bulk", s.assignMultipleMiddlewares) // New endpoint for bulk assignment resources.DELETE("/:id/middlewares/:middlewareId", s.removeMiddleware) } } diff --git a/config/templates.yaml b/config/templates.yaml index 7522cbbd6..637535bfe 100644 --- a/config/templates.yaml +++ b/config/templates.yaml @@ -91,4 +91,11 @@ middlewares: X-Forwarded-Proto: https stsIncludeSubdomains: true stsPreload: true - stsSeconds: 63072000 \ No newline at end of file + stsSeconds: 63072000 + + - id: nextcloud-dav + name: Nextcloud WebDAV Redirect + type: replacepathregex + config: + regex: "^/.well-known/ca(l|rd)dav" + replacement: "/remote.php/dav/" \ No newline at end of file diff --git a/ui/src/App.js b/ui/src/App.js index 4535d3597..c42779c35 100644 --- a/ui/src/App.js +++ b/ui/src/App.js @@ -15,6 +15,11 @@ const api = { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }).then(res => res.json()), + assignMultipleMiddlewares: (resourceId, data) => fetch(`${API_URL}/resources/${resourceId}/middlewares/bulk`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }).then(res => res.json()), removeMiddleware: (resourceId, middlewareId) => fetch(`${API_URL}/resources/${resourceId}/middlewares/${middlewareId}`, { method: 'DELETE' }).then(res => res.json()), @@ -520,7 +525,7 @@ const ResourceDetail = ({ id, navigateTo }) => { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [showModal, setShowModal] = useState(false); - const [selectedMiddleware, setSelectedMiddleware] = useState(''); + const [selectedMiddlewares, setSelectedMiddlewares] = useState([]); const [priority, setPriority] = useState(100); const fetchData = async () => { @@ -555,24 +560,42 @@ const ResourceDetail = ({ id, navigateTo }) => { fetchData(); }, [id]); + const handleMiddlewareSelection = (e) => { + const options = e.target.options; + const selected = []; + for (let i = 0; i < options.length; i++) { + if (options[i].selected) { + selected.push(options[i].value); + } + } + setSelectedMiddlewares(selected); + }; + const handleAssignMiddleware = async (e) => { e.preventDefault(); - if (!selectedMiddleware) return; + if (selectedMiddlewares.length === 0) { + alert('Please select at least one middleware'); + return; + } try { - await api.assignMiddleware(id, { - middleware_id: selectedMiddleware, + const middlewaresToAdd = selectedMiddlewares.map(middlewareId => ({ + middleware_id: middlewareId, priority: parseInt(priority) + })); + + await api.assignMultipleMiddlewares(id, { + middlewares: middlewaresToAdd }); setShowModal(false); - setSelectedMiddleware(''); + setSelectedMiddlewares([]); setPriority(100); // Refresh data fetchData(); } catch (err) { - alert('Failed to assign middleware'); + alert('Failed to assign middlewares'); console.error(err); } }; @@ -788,7 +811,7 @@ const ResourceDetail = ({ id, navigateTo }) => {
-

Add Middleware to {resource.host}

+

Add Middlewares to {resource.host}

@@ -1026,6 +1053,7 @@ const MiddlewareForm = ({ id, isEditing, navigateTo }) => { { value: 'redirectRegex', label: 'Redirect Regex' }, { value: 'redirectScheme', label: 'Redirect Scheme' }, { value: 'chain', label: 'Middleware Chain' }, + { value: 'replacepathregex', label: 'RegEx Path Replacement' }, ]; // Templates for different middleware types @@ -1057,6 +1085,15 @@ const MiddlewareForm = ({ id, isEditing, navigateTo }) => { }, chain: { middlewares: [] + }, + replacepathregex: { + regex: "^/path/to/replace", + replacement: "/new/path" + }, + redirectRegex: { + regex: "^/path/to/redirect", + replacement: "/new/path", + permanent: false } }; @@ -1179,6 +1216,32 @@ const MiddlewareForm = ({ id, isEditing, navigateTo }) => { setSelectedMiddlewares(selected); }; + // Helper text for middleware types + const getTypeHelperText = () => { + switch (type) { + case 'replacepathregex': + return ( +
+

The RegEx Path Replacement middleware rewrites the URL path based on regex match.

+

Common use cases:

+
    +
  • WebDAV redirects: ^/.well-known/ca(l|rd)dav/remote.php/dav/
  • +
  • Hiding paths: ^/api/internal/(.*)/api/public/$1
  • +
+
+ ); + case 'chain': + return ( +
+

The Chain middleware allows you to combine multiple middlewares together.

+

Order matters - middlewares are processed from top to bottom.

+
+ ); + default: + return null; + } + }; + const handleSubmit = async (e) => { e.preventDefault(); @@ -1281,6 +1344,7 @@ const MiddlewareForm = ({ id, isEditing, navigateTo }) => { ))} + {getTypeHelperText()}
{/* Chain specific UI */}
NameTypeMiddleware Priority Actions
{middleware.name}{middlewareDetails ? middlewareDetails.type : 'Unknown'} + {formatMiddlewareDisplay(middlewareDetails, middlewares)} + {middleware.priority}
{resource.host}
+ {resource.host} + {isDisabled && ( + + Removed from Pangolin + + )} + - - {isProtected ? 'Protected' : 'Not Protected'} + + {isDisabled ? 'Disabled' : isProtected ? 'Protected' : 'Not Protected'} {middlewaresList.length > 0 ? middlewaresList.length : 'None'} + {isDisabled && ( + + )}
{resource.host}
+ {resource.host} + {isDisabled && ( + + Removed from Pangolin + + )} + - - {isProtected ? 'Protected' : 'Not Protected'} + + {isDisabled ? 'Disabled' : isProtected ? 'Protected' : 'Not Protected'} {middlewaresList.length > 0 ? middlewaresList.length : 'None'} + + {isDisabled && ( + + )}