From dd93f9a2c4f6a3476f178e9da83acb088ff477a5 Mon Sep 17 00:00:00 2001 From: Anthony Rubick <68485672+AnthonyMichaelTDM@users.noreply.github.com> Date: Thu, 17 Jul 2025 19:50:57 -0700 Subject: [PATCH 01/12] feat(plugins): Implement plugin API key management and authentication middleware The purpose of this is to allow plugins to access certain internal APIs via - Added PluginAPIKey and APIKeyManager for managing API keys associated with plugins. - Introduced PluginAuthMiddleware to handle API key validation for plugin requests. - Updated RouterDef to support plugin accessible endpoints with authentication. - Modified various API registration functions to include plugin accessibility checks. - Enhanced plugin lifecycle management to generate and revoke API keys as needed. - Updated plugin specifications to include permitted API endpoints for access control. --- src/api.go | 326 +++++++++--------- src/def.go | 3 + src/mod/auth/auth.go | 8 +- src/mod/auth/plugin_apikey_manager.go | 152 ++++++++ src/mod/auth/plugin_middleware.go | 77 +++++ src/mod/auth/router.go | 43 ++- src/mod/plugins/lifecycle.go | 19 + src/mod/plugins/typdef.go | 5 + .../plugins/zoraxy_plugin/zoraxy_plugin.go | 17 +- src/start.go | 11 +- 10 files changed, 470 insertions(+), 191 deletions(-) create mode 100644 src/mod/auth/plugin_apikey_manager.go create mode 100644 src/mod/auth/plugin_middleware.go diff --git a/src/api.go b/src/api.go index 17fd3c4..b93c351 100644 --- a/src/api.go +++ b/src/api.go @@ -25,233 +25,233 @@ import ( // Register the APIs for HTTP proxy management functions func RegisterHTTPProxyAPIs(authRouter *auth.RouterDef) { /* Reverse Proxy Settings & Status */ - authRouter.HandleFunc("/api/proxy/enable", ReverseProxyHandleOnOff) - authRouter.HandleFunc("/api/proxy/add", ReverseProxyHandleAddEndpoint) - authRouter.HandleFunc("/api/proxy/status", ReverseProxyStatus) - authRouter.HandleFunc("/api/proxy/toggle", ReverseProxyToggleRuleSet) - authRouter.HandleFunc("/api/proxy/list", ReverseProxyList) - authRouter.HandleFunc("/api/proxy/listTags", ReverseProxyListTags) - authRouter.HandleFunc("/api/proxy/detail", ReverseProxyListDetail) - authRouter.HandleFunc("/api/proxy/edit", ReverseProxyHandleEditEndpoint) - authRouter.HandleFunc("/api/proxy/setAlias", ReverseProxyHandleAlias) - authRouter.HandleFunc("/api/proxy/setTlsConfig", ReverseProxyHandleSetTlsConfig) - authRouter.HandleFunc("/api/proxy/setHostname", ReverseProxyHandleSetHostname) - authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint) - authRouter.HandleFunc("/api/proxy/updateCredentials", UpdateProxyBasicAuthCredentials) - authRouter.HandleFunc("/api/proxy/tlscheck", domainsniff.HandleCheckSiteSupportTLS) - authRouter.HandleFunc("/api/proxy/setIncoming", HandleIncomingPortSet) - authRouter.HandleFunc("/api/proxy/useHttpsRedirect", HandleUpdateHttpsRedirect) - authRouter.HandleFunc("/api/proxy/listenPort80", HandleUpdatePort80Listener) - authRouter.HandleFunc("/api/proxy/requestIsProxied", HandleManagementProxyCheck) - authRouter.HandleFunc("/api/proxy/developmentMode", HandleDevelopmentModeChange) + authRouter.HandleFunc("/api/proxy/enable", ReverseProxyHandleOnOff, false) + authRouter.HandleFunc("/api/proxy/add", ReverseProxyHandleAddEndpoint, false) + authRouter.HandleFunc("/api/proxy/status", ReverseProxyStatus, true) + authRouter.HandleFunc("/api/proxy/toggle", ReverseProxyToggleRuleSet, false) + authRouter.HandleFunc("/api/proxy/list", ReverseProxyList, true) + authRouter.HandleFunc("/api/proxy/listTags", ReverseProxyListTags, true) + authRouter.HandleFunc("/api/proxy/detail", ReverseProxyListDetail, true) + authRouter.HandleFunc("/api/proxy/edit", ReverseProxyHandleEditEndpoint, false) + authRouter.HandleFunc("/api/proxy/setAlias", ReverseProxyHandleAlias, false) + authRouter.HandleFunc("/api/proxy/setTlsConfig", ReverseProxyHandleSetTlsConfig, false) + authRouter.HandleFunc("/api/proxy/setHostname", ReverseProxyHandleSetHostname, false) + authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint, false) + authRouter.HandleFunc("/api/proxy/updateCredentials", UpdateProxyBasicAuthCredentials, false) + authRouter.HandleFunc("/api/proxy/tlscheck", domainsniff.HandleCheckSiteSupportTLS, false) + authRouter.HandleFunc("/api/proxy/setIncoming", HandleIncomingPortSet, false) + authRouter.HandleFunc("/api/proxy/useHttpsRedirect", HandleUpdateHttpsRedirect, false) + authRouter.HandleFunc("/api/proxy/listenPort80", HandleUpdatePort80Listener, false) + authRouter.HandleFunc("/api/proxy/requestIsProxied", HandleManagementProxyCheck, false) + authRouter.HandleFunc("/api/proxy/developmentMode", HandleDevelopmentModeChange, false) /* Reverse proxy upstream (load balance) */ - authRouter.HandleFunc("/api/proxy/upstream/list", ReverseProxyUpstreamList) - authRouter.HandleFunc("/api/proxy/upstream/add", ReverseProxyUpstreamAdd) - authRouter.HandleFunc("/api/proxy/upstream/setPriority", ReverseProxyUpstreamSetPriority) - authRouter.HandleFunc("/api/proxy/upstream/update", ReverseProxyUpstreamUpdate) - authRouter.HandleFunc("/api/proxy/upstream/remove", ReverseProxyUpstreamDelete) + authRouter.HandleFunc("/api/proxy/upstream/list", ReverseProxyUpstreamList, false) + authRouter.HandleFunc("/api/proxy/upstream/add", ReverseProxyUpstreamAdd, false) + authRouter.HandleFunc("/api/proxy/upstream/setPriority", ReverseProxyUpstreamSetPriority, false) + authRouter.HandleFunc("/api/proxy/upstream/update", ReverseProxyUpstreamUpdate, false) + authRouter.HandleFunc("/api/proxy/upstream/remove", ReverseProxyUpstreamDelete, false) /* Reverse proxy virtual directory */ - authRouter.HandleFunc("/api/proxy/vdir/list", ReverseProxyListVdir) - authRouter.HandleFunc("/api/proxy/vdir/add", ReverseProxyAddVdir) - authRouter.HandleFunc("/api/proxy/vdir/del", ReverseProxyDeleteVdir) - authRouter.HandleFunc("/api/proxy/vdir/edit", ReverseProxyEditVdir) + authRouter.HandleFunc("/api/proxy/vdir/list", ReverseProxyListVdir, false) + authRouter.HandleFunc("/api/proxy/vdir/add", ReverseProxyAddVdir, false) + authRouter.HandleFunc("/api/proxy/vdir/del", ReverseProxyDeleteVdir, false) + authRouter.HandleFunc("/api/proxy/vdir/edit", ReverseProxyEditVdir, false) /* Reverse proxy user-defined header */ - authRouter.HandleFunc("/api/proxy/header/list", HandleCustomHeaderList) - authRouter.HandleFunc("/api/proxy/header/add", HandleCustomHeaderAdd) - authRouter.HandleFunc("/api/proxy/header/remove", HandleCustomHeaderRemove) - authRouter.HandleFunc("/api/proxy/header/handleHSTS", HandleHSTSState) - authRouter.HandleFunc("/api/proxy/header/handleHopByHop", HandleHopByHop) - authRouter.HandleFunc("/api/proxy/header/handleHostOverwrite", HandleHostOverwrite) - authRouter.HandleFunc("/api/proxy/header/handlePermissionPolicy", HandlePermissionPolicy) - authRouter.HandleFunc("/api/proxy/header/handleWsHeaderBehavior", HandleWsHeaderBehavior) + authRouter.HandleFunc("/api/proxy/header/list", HandleCustomHeaderList, true) + authRouter.HandleFunc("/api/proxy/header/add", HandleCustomHeaderAdd, false) + authRouter.HandleFunc("/api/proxy/header/remove", HandleCustomHeaderRemove, false) + authRouter.HandleFunc("/api/proxy/header/handleHSTS", HandleHSTSState, false) + authRouter.HandleFunc("/api/proxy/header/handleHopByHop", HandleHopByHop, false) + authRouter.HandleFunc("/api/proxy/header/handleHostOverwrite", HandleHostOverwrite, false) + authRouter.HandleFunc("/api/proxy/header/handlePermissionPolicy", HandlePermissionPolicy, false) + authRouter.HandleFunc("/api/proxy/header/handleWsHeaderBehavior", HandleWsHeaderBehavior, false) /* Reverse proxy auth related */ - authRouter.HandleFunc("/api/proxy/auth/exceptions/list", ListProxyBasicAuthExceptionPaths) - authRouter.HandleFunc("/api/proxy/auth/exceptions/add", AddProxyBasicAuthExceptionPaths) - authRouter.HandleFunc("/api/proxy/auth/exceptions/delete", RemoveProxyBasicAuthExceptionPaths) + authRouter.HandleFunc("/api/proxy/auth/exceptions/list", ListProxyBasicAuthExceptionPaths, true) + authRouter.HandleFunc("/api/proxy/auth/exceptions/add", AddProxyBasicAuthExceptionPaths, false) + authRouter.HandleFunc("/api/proxy/auth/exceptions/delete", RemoveProxyBasicAuthExceptionPaths, false) } // Register the APIs for TLS / SSL certificate management functions func RegisterTLSAPIs(authRouter *auth.RouterDef) { //Global certificate settings - authRouter.HandleFunc("/api/cert/tls", handleToggleTLSProxy) - authRouter.HandleFunc("/api/cert/tlsRequireLatest", handleSetTlsRequireLatest) - authRouter.HandleFunc("/api/cert/resolve", handleCertTryResolve) - authRouter.HandleFunc("/api/cert/setPreferredCertificate", handleSetDomainPreferredCertificate) + authRouter.HandleFunc("/api/cert/tls", handleToggleTLSProxy, false) + authRouter.HandleFunc("/api/cert/tlsRequireLatest", handleSetTlsRequireLatest, false) + authRouter.HandleFunc("/api/cert/resolve", handleCertTryResolve, false) + authRouter.HandleFunc("/api/cert/setPreferredCertificate", handleSetDomainPreferredCertificate, false) //Certificate store functions - authRouter.HandleFunc("/api/cert/upload", tlsCertManager.HandleCertUpload) - authRouter.HandleFunc("/api/cert/download", tlsCertManager.HandleCertDownload) - authRouter.HandleFunc("/api/cert/list", tlsCertManager.HandleListCertificate) - authRouter.HandleFunc("/api/cert/listdomains", tlsCertManager.HandleListDomains) - authRouter.HandleFunc("/api/cert/checkDefault", tlsCertManager.HandleDefaultCertCheck) - authRouter.HandleFunc("/api/cert/delete", tlsCertManager.HandleCertRemove) - authRouter.HandleFunc("/api/cert/selfsign", tlsCertManager.HandleSelfSignCertGenerate) + authRouter.HandleFunc("/api/cert/upload", tlsCertManager.HandleCertUpload, false) + authRouter.HandleFunc("/api/cert/download", tlsCertManager.HandleCertDownload, false) + authRouter.HandleFunc("/api/cert/list", tlsCertManager.HandleListCertificate, false) + authRouter.HandleFunc("/api/cert/listdomains", tlsCertManager.HandleListDomains, false) + authRouter.HandleFunc("/api/cert/checkDefault", tlsCertManager.HandleDefaultCertCheck, false) + authRouter.HandleFunc("/api/cert/delete", tlsCertManager.HandleCertRemove, false) + authRouter.HandleFunc("/api/cert/selfsign", tlsCertManager.HandleSelfSignCertGenerate, false) } // Register the APIs for Authentication handlers like Forward Auth and OAUTH2 func RegisterAuthenticationHandlerAPIs(authRouter *auth.RouterDef) { - authRouter.HandleFunc("/api/sso/forward-auth", forwardAuthRouter.HandleAPIOptions) - authRouter.HandleFunc("/api/sso/OAuth2", oauth2Router.HandleSetOAuth2Settings) + authRouter.HandleFunc("/api/sso/forward-auth", forwardAuthRouter.HandleAPIOptions, false) + authRouter.HandleFunc("/api/sso/OAuth2", oauth2Router.HandleSetOAuth2Settings, false) } // Register the APIs for redirection rules management functions func RegisterRedirectionAPIs(authRouter *auth.RouterDef) { - authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules) - authRouter.HandleFunc("/api/redirect/add", handleAddRedirectionRule) - authRouter.HandleFunc("/api/redirect/delete", handleDeleteRedirectionRule) - authRouter.HandleFunc("/api/redirect/edit", handleEditRedirectionRule) - authRouter.HandleFunc("/api/redirect/regex", handleToggleRedirectRegexpSupport) + authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules, true) + authRouter.HandleFunc("/api/redirect/add", handleAddRedirectionRule, false) + authRouter.HandleFunc("/api/redirect/delete", handleDeleteRedirectionRule, false) + authRouter.HandleFunc("/api/redirect/edit", handleEditRedirectionRule, false) + authRouter.HandleFunc("/api/redirect/regex", handleToggleRedirectRegexpSupport, false) } // Register the APIs for access rules management functions func RegisterAccessRuleAPIs(authRouter *auth.RouterDef) { /* Access Rules Settings & Status */ - authRouter.HandleFunc("/api/access/list", handleListAccessRules) - authRouter.HandleFunc("/api/access/attach", handleAttachRuleToHost) - authRouter.HandleFunc("/api/access/create", handleCreateAccessRule) - authRouter.HandleFunc("/api/access/remove", handleRemoveAccessRule) - authRouter.HandleFunc("/api/access/update", handleUpadateAccessRule) + authRouter.HandleFunc("/api/access/list", handleListAccessRules, true) + authRouter.HandleFunc("/api/access/attach", handleAttachRuleToHost, false) + authRouter.HandleFunc("/api/access/create", handleCreateAccessRule, false) + authRouter.HandleFunc("/api/access/remove", handleRemoveAccessRule, false) + authRouter.HandleFunc("/api/access/update", handleUpadateAccessRule, false) /* Blacklist */ - authRouter.HandleFunc("/api/blacklist/list", handleListBlacklisted) - authRouter.HandleFunc("/api/blacklist/country/add", handleCountryBlacklistAdd) - authRouter.HandleFunc("/api/blacklist/country/remove", handleCountryBlacklistRemove) - authRouter.HandleFunc("/api/blacklist/ip/add", handleIpBlacklistAdd) - authRouter.HandleFunc("/api/blacklist/ip/remove", handleIpBlacklistRemove) - authRouter.HandleFunc("/api/blacklist/enable", handleBlacklistEnable) + authRouter.HandleFunc("/api/blacklist/list", handleListBlacklisted, true) + authRouter.HandleFunc("/api/blacklist/country/add", handleCountryBlacklistAdd, false) + authRouter.HandleFunc("/api/blacklist/country/remove", handleCountryBlacklistRemove, false) + authRouter.HandleFunc("/api/blacklist/ip/add", handleIpBlacklistAdd, true) + authRouter.HandleFunc("/api/blacklist/ip/remove", handleIpBlacklistRemove, true) + authRouter.HandleFunc("/api/blacklist/enable", handleBlacklistEnable, true) /* Whitelist */ - authRouter.HandleFunc("/api/whitelist/list", handleListWhitelisted) - authRouter.HandleFunc("/api/whitelist/country/add", handleCountryWhitelistAdd) - authRouter.HandleFunc("/api/whitelist/country/remove", handleCountryWhitelistRemove) - authRouter.HandleFunc("/api/whitelist/ip/add", handleIpWhitelistAdd) - authRouter.HandleFunc("/api/whitelist/ip/remove", handleIpWhitelistRemove) - authRouter.HandleFunc("/api/whitelist/enable", handleWhitelistEnable) - authRouter.HandleFunc("/api/whitelist/allowLocal", handleWhitelistAllowLoopback) + authRouter.HandleFunc("/api/whitelist/list", handleListWhitelisted, true) + authRouter.HandleFunc("/api/whitelist/country/add", handleCountryWhitelistAdd, false) + authRouter.HandleFunc("/api/whitelist/country/remove", handleCountryWhitelistRemove, false) + authRouter.HandleFunc("/api/whitelist/ip/add", handleIpWhitelistAdd, false) + authRouter.HandleFunc("/api/whitelist/ip/remove", handleIpWhitelistRemove, false) + authRouter.HandleFunc("/api/whitelist/enable", handleWhitelistEnable, false) + authRouter.HandleFunc("/api/whitelist/allowLocal", handleWhitelistAllowLoopback, false) /* Quick Ban List */ - authRouter.HandleFunc("/api/quickban/list", handleListQuickBan) + authRouter.HandleFunc("/api/quickban/list", handleListQuickBan, false) } // Register the APIs for path blocking rules management functions, WIP func RegisterPathRuleAPIs(authRouter *auth.RouterDef) { - authRouter.HandleFunc("/api/pathrule/add", pathRuleHandler.HandleAddBlockingPath) - authRouter.HandleFunc("/api/pathrule/list", pathRuleHandler.HandleListBlockingPath) - authRouter.HandleFunc("/api/pathrule/remove", pathRuleHandler.HandleRemoveBlockingPath) + authRouter.HandleFunc("/api/pathrule/add", pathRuleHandler.HandleAddBlockingPath, false) + authRouter.HandleFunc("/api/pathrule/list", pathRuleHandler.HandleListBlockingPath, false) + authRouter.HandleFunc("/api/pathrule/remove", pathRuleHandler.HandleRemoveBlockingPath, false) } // Register the APIs statistic anlysis and uptime monitoring functions func RegisterStatisticalAPIs(authRouter *auth.RouterDef) { /* Traffic Summary */ - authRouter.HandleFunc("/api/stats/summary", statisticCollector.HandleTodayStatLoad) - authRouter.HandleFunc("/api/stats/countries", HandleCountryDistrSummary) - authRouter.HandleFunc("/api/stats/netstat", netstatBuffers.HandleGetNetworkInterfaceStats) - authRouter.HandleFunc("/api/stats/netstatgraph", netstatBuffers.HandleGetBufferedNetworkInterfaceStats) - authRouter.HandleFunc("/api/stats/listnic", netstat.HandleListNetworkInterfaces) + authRouter.HandleFunc("/api/stats/summary", statisticCollector.HandleTodayStatLoad, false) + authRouter.HandleFunc("/api/stats/countries", HandleCountryDistrSummary, false) + authRouter.HandleFunc("/api/stats/netstat", netstatBuffers.HandleGetNetworkInterfaceStats, false) + authRouter.HandleFunc("/api/stats/netstatgraph", netstatBuffers.HandleGetBufferedNetworkInterfaceStats, false) + authRouter.HandleFunc("/api/stats/listnic", netstat.HandleListNetworkInterfaces, false) /* Zoraxy Analytic */ - authRouter.HandleFunc("/api/analytic/list", AnalyticLoader.HandleSummaryList) - authRouter.HandleFunc("/api/analytic/load", AnalyticLoader.HandleLoadTargetDaySummary) - authRouter.HandleFunc("/api/analytic/loadRange", AnalyticLoader.HandleLoadTargetRangeSummary) - authRouter.HandleFunc("/api/analytic/exportRange", AnalyticLoader.HandleRangeExport) - authRouter.HandleFunc("/api/analytic/resetRange", AnalyticLoader.HandleRangeReset) + authRouter.HandleFunc("/api/analytic/list", AnalyticLoader.HandleSummaryList, false) + authRouter.HandleFunc("/api/analytic/load", AnalyticLoader.HandleLoadTargetDaySummary, false) + authRouter.HandleFunc("/api/analytic/loadRange", AnalyticLoader.HandleLoadTargetRangeSummary, false) + authRouter.HandleFunc("/api/analytic/exportRange", AnalyticLoader.HandleRangeExport, false) + authRouter.HandleFunc("/api/analytic/resetRange", AnalyticLoader.HandleRangeReset, false) /* UpTime Monitor */ - authRouter.HandleFunc("/api/utm/list", HandleUptimeMonitorListing) + authRouter.HandleFunc("/api/utm/list", HandleUptimeMonitorListing, false) } // Register the APIs for Stream (TCP / UDP) Proxy management functions func RegisterStreamProxyAPIs(authRouter *auth.RouterDef) { - authRouter.HandleFunc("/api/streamprox/config/add", streamProxyManager.HandleAddProxyConfig) - authRouter.HandleFunc("/api/streamprox/config/edit", streamProxyManager.HandleEditProxyConfigs) - authRouter.HandleFunc("/api/streamprox/config/list", streamProxyManager.HandleListConfigs) - authRouter.HandleFunc("/api/streamprox/config/start", streamProxyManager.HandleStartProxy) - authRouter.HandleFunc("/api/streamprox/config/stop", streamProxyManager.HandleStopProxy) - authRouter.HandleFunc("/api/streamprox/config/delete", streamProxyManager.HandleRemoveProxy) - authRouter.HandleFunc("/api/streamprox/config/status", streamProxyManager.HandleGetProxyStatus) + authRouter.HandleFunc("/api/streamprox/config/add", streamProxyManager.HandleAddProxyConfig, false) + authRouter.HandleFunc("/api/streamprox/config/edit", streamProxyManager.HandleEditProxyConfigs, false) + authRouter.HandleFunc("/api/streamprox/config/list", streamProxyManager.HandleListConfigs, false) + authRouter.HandleFunc("/api/streamprox/config/start", streamProxyManager.HandleStartProxy, false) + authRouter.HandleFunc("/api/streamprox/config/stop", streamProxyManager.HandleStopProxy, false) + authRouter.HandleFunc("/api/streamprox/config/delete", streamProxyManager.HandleRemoveProxy, false) + authRouter.HandleFunc("/api/streamprox/config/status", streamProxyManager.HandleGetProxyStatus, false) } // Register the APIs for mDNS service management functions func RegisterMDNSAPIs(authRouter *auth.RouterDef) { - authRouter.HandleFunc("/api/mdns/list", HandleMdnsListing) - authRouter.HandleFunc("/api/mdns/discover", HandleMdnsScanning) + authRouter.HandleFunc("/api/mdns/list", HandleMdnsListing, false) + authRouter.HandleFunc("/api/mdns/discover", HandleMdnsScanning, false) } // Register the APIs for ACME and Auto Renewer management functions func RegisterACMEAndAutoRenewerAPIs(authRouter *auth.RouterDef) { /* ACME Core */ - authRouter.HandleFunc("/api/acme/listExpiredDomains", acmeHandler.HandleGetExpiredDomains) - authRouter.HandleFunc("/api/acme/obtainCert", AcmeCheckAndHandleRenewCertificate) + authRouter.HandleFunc("/api/acme/listExpiredDomains", acmeHandler.HandleGetExpiredDomains, false) + authRouter.HandleFunc("/api/acme/obtainCert", AcmeCheckAndHandleRenewCertificate, false) /* Auto Renewer */ - authRouter.HandleFunc("/api/acme/autoRenew/enable", acmeAutoRenewer.HandleAutoRenewEnable) - authRouter.HandleFunc("/api/acme/autoRenew/ca", HandleACMEPreferredCA) - authRouter.HandleFunc("/api/acme/autoRenew/email", acmeAutoRenewer.HandleACMEEmail) - authRouter.HandleFunc("/api/acme/autoRenew/setDomains", acmeAutoRenewer.HandleSetAutoRenewDomains) - authRouter.HandleFunc("/api/acme/autoRenew/setEAB", acmeAutoRenewer.HanldeSetEAB) - authRouter.HandleFunc("/api/acme/autoRenew/setDNS", acmeAutoRenewer.HandleSetDNS) - authRouter.HandleFunc("/api/acme/autoRenew/listDomains", acmeAutoRenewer.HandleLoadAutoRenewDomains) - authRouter.HandleFunc("/api/acme/autoRenew/renewPolicy", acmeAutoRenewer.HandleRenewPolicy) - authRouter.HandleFunc("/api/acme/autoRenew/renewNow", acmeAutoRenewer.HandleRenewNow) - authRouter.HandleFunc("/api/acme/dns/providers", acmedns.HandleServeProvidersJson) + authRouter.HandleFunc("/api/acme/autoRenew/enable", acmeAutoRenewer.HandleAutoRenewEnable, false) + authRouter.HandleFunc("/api/acme/autoRenew/ca", HandleACMEPreferredCA, false) + authRouter.HandleFunc("/api/acme/autoRenew/email", acmeAutoRenewer.HandleACMEEmail, false) + authRouter.HandleFunc("/api/acme/autoRenew/setDomains", acmeAutoRenewer.HandleSetAutoRenewDomains, false) + authRouter.HandleFunc("/api/acme/autoRenew/setEAB", acmeAutoRenewer.HanldeSetEAB, false) + authRouter.HandleFunc("/api/acme/autoRenew/setDNS", acmeAutoRenewer.HandleSetDNS, false) + authRouter.HandleFunc("/api/acme/autoRenew/listDomains", acmeAutoRenewer.HandleLoadAutoRenewDomains, false) + authRouter.HandleFunc("/api/acme/autoRenew/renewPolicy", acmeAutoRenewer.HandleRenewPolicy, false) + authRouter.HandleFunc("/api/acme/autoRenew/renewNow", acmeAutoRenewer.HandleRenewNow, false) + authRouter.HandleFunc("/api/acme/dns/providers", acmedns.HandleServeProvidersJson, false) /* ACME Wizard */ - authRouter.HandleFunc("/api/acme/wizard", acmewizard.HandleGuidedStepCheck) + authRouter.HandleFunc("/api/acme/wizard", acmewizard.HandleGuidedStepCheck, false) } // Register the APIs for Static Web Server management functions func RegisterStaticWebServerAPIs(authRouter *auth.RouterDef) { /* Static Web Server Controls */ - authRouter.HandleFunc("/api/webserv/status", staticWebServer.HandleGetStatus) - authRouter.HandleFunc("/api/webserv/start", staticWebServer.HandleStartServer) - authRouter.HandleFunc("/api/webserv/stop", staticWebServer.HandleStopServer) - authRouter.HandleFunc("/api/webserv/setPort", HandleStaticWebServerPortChange) - authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing) - authRouter.HandleFunc("/api/webserv/disableListenAllInterface", staticWebServer.SetDisableListenToAllInterface) + authRouter.HandleFunc("/api/webserv/status", staticWebServer.HandleGetStatus, false) + authRouter.HandleFunc("/api/webserv/start", staticWebServer.HandleStartServer, false) + authRouter.HandleFunc("/api/webserv/stop", staticWebServer.HandleStopServer, false) + authRouter.HandleFunc("/api/webserv/setPort", HandleStaticWebServerPortChange, false) + authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing, false) + authRouter.HandleFunc("/api/webserv/disableListenAllInterface", staticWebServer.SetDisableListenToAllInterface, false) /* File Manager */ if *allowWebFileManager { - authRouter.HandleFunc("/api/fs/list", staticWebServer.FileManager.HandleList) - authRouter.HandleFunc("/api/fs/upload", staticWebServer.FileManager.HandleUpload) - authRouter.HandleFunc("/api/fs/download", staticWebServer.FileManager.HandleDownload) - authRouter.HandleFunc("/api/fs/newFolder", staticWebServer.FileManager.HandleNewFolder) - authRouter.HandleFunc("/api/fs/copy", staticWebServer.FileManager.HandleFileCopy) - authRouter.HandleFunc("/api/fs/move", staticWebServer.FileManager.HandleFileMove) - authRouter.HandleFunc("/api/fs/properties", staticWebServer.FileManager.HandleFileProperties) - authRouter.HandleFunc("/api/fs/del", staticWebServer.FileManager.HandleFileDelete) + authRouter.HandleFunc("/api/fs/list", staticWebServer.FileManager.HandleList, false) + authRouter.HandleFunc("/api/fs/upload", staticWebServer.FileManager.HandleUpload, false) + authRouter.HandleFunc("/api/fs/download", staticWebServer.FileManager.HandleDownload, false) + authRouter.HandleFunc("/api/fs/newFolder", staticWebServer.FileManager.HandleNewFolder, false) + authRouter.HandleFunc("/api/fs/copy", staticWebServer.FileManager.HandleFileCopy, false) + authRouter.HandleFunc("/api/fs/move", staticWebServer.FileManager.HandleFileMove, false) + authRouter.HandleFunc("/api/fs/properties", staticWebServer.FileManager.HandleFileProperties, false) + authRouter.HandleFunc("/api/fs/del", staticWebServer.FileManager.HandleFileDelete, false) } } // Register the APIs for Network Utilities functions func RegisterNetworkUtilsAPIs(authRouter *auth.RouterDef) { - authRouter.HandleFunc("/api/tools/ipscan", ipscan.HandleIpScan) - authRouter.HandleFunc("/api/tools/portscan", ipscan.HandleScanPort) - authRouter.HandleFunc("/api/tools/traceroute", netutils.HandleTraceRoute) - authRouter.HandleFunc("/api/tools/ping", netutils.HandlePing) - authRouter.HandleFunc("/api/tools/whois", netutils.HandleWhois) - authRouter.HandleFunc("/api/tools/webssh", HandleCreateProxySession) - authRouter.HandleFunc("/api/tools/websshSupported", HandleWebSshSupportCheck) - authRouter.HandleFunc("/api/tools/wol", HandleWakeOnLan) - authRouter.HandleFunc("/api/tools/smtp/get", HandleSMTPGet) - authRouter.HandleFunc("/api/tools/smtp/set", HandleSMTPSet) - authRouter.HandleFunc("/api/tools/smtp/admin", HandleAdminEmailGet) - authRouter.HandleFunc("/api/tools/smtp/test", HandleTestEmailSend) - authRouter.HandleFunc("/api/tools/fwdproxy/enable", forwardProxy.HandleToogle) - authRouter.HandleFunc("/api/tools/fwdproxy/port", forwardProxy.HandlePort) + authRouter.HandleFunc("/api/tools/ipscan", ipscan.HandleIpScan, false) + authRouter.HandleFunc("/api/tools/portscan", ipscan.HandleScanPort, false) + authRouter.HandleFunc("/api/tools/traceroute", netutils.HandleTraceRoute, false) + authRouter.HandleFunc("/api/tools/ping", netutils.HandlePing, false) + authRouter.HandleFunc("/api/tools/whois", netutils.HandleWhois, false) + authRouter.HandleFunc("/api/tools/webssh", HandleCreateProxySession, false) + authRouter.HandleFunc("/api/tools/websshSupported", HandleWebSshSupportCheck, false) + authRouter.HandleFunc("/api/tools/wol", HandleWakeOnLan, false) + authRouter.HandleFunc("/api/tools/smtp/get", HandleSMTPGet, false) + authRouter.HandleFunc("/api/tools/smtp/set", HandleSMTPSet, false) + authRouter.HandleFunc("/api/tools/smtp/admin", HandleAdminEmailGet, false) + authRouter.HandleFunc("/api/tools/smtp/test", HandleTestEmailSend, false) + authRouter.HandleFunc("/api/tools/fwdproxy/enable", forwardProxy.HandleToogle, false) + authRouter.HandleFunc("/api/tools/fwdproxy/port", forwardProxy.HandlePort, false) } func RegisterPluginAPIs(authRouter *auth.RouterDef) { - authRouter.HandleFunc("/api/plugins/list", pluginManager.HandleListPlugins) - authRouter.HandleFunc("/api/plugins/enable", pluginManager.HandleEnablePlugin) - authRouter.HandleFunc("/api/plugins/disable", pluginManager.HandleDisablePlugin) - authRouter.HandleFunc("/api/plugins/icon", pluginManager.HandleLoadPluginIcon) - authRouter.HandleFunc("/api/plugins/info", pluginManager.HandlePluginInfo) + authRouter.HandleFunc("/api/plugins/list", pluginManager.HandleListPlugins, false) + authRouter.HandleFunc("/api/plugins/enable", pluginManager.HandleEnablePlugin, false) + authRouter.HandleFunc("/api/plugins/disable", pluginManager.HandleDisablePlugin, false) + authRouter.HandleFunc("/api/plugins/icon", pluginManager.HandleLoadPluginIcon, false) + authRouter.HandleFunc("/api/plugins/info", pluginManager.HandlePluginInfo, false) - authRouter.HandleFunc("/api/plugins/groups/list", pluginManager.HandleListPluginGroups) - authRouter.HandleFunc("/api/plugins/groups/add", pluginManager.HandleAddPluginToGroup) - authRouter.HandleFunc("/api/plugins/groups/remove", pluginManager.HandleRemovePluginFromGroup) - authRouter.HandleFunc("/api/plugins/groups/deleteTag", pluginManager.HandleRemovePluginGroup) + authRouter.HandleFunc("/api/plugins/groups/list", pluginManager.HandleListPluginGroups, false) + authRouter.HandleFunc("/api/plugins/groups/add", pluginManager.HandleAddPluginToGroup, false) + authRouter.HandleFunc("/api/plugins/groups/remove", pluginManager.HandleRemovePluginFromGroup, false) + authRouter.HandleFunc("/api/plugins/groups/deleteTag", pluginManager.HandleRemovePluginGroup, false) - authRouter.HandleFunc("/api/plugins/store/list", pluginManager.HandleListDownloadablePlugins) - authRouter.HandleFunc("/api/plugins/store/resync", pluginManager.HandleResyncPluginList) - authRouter.HandleFunc("/api/plugins/store/install", pluginManager.HandleInstallPlugin) - authRouter.HandleFunc("/api/plugins/store/uninstall", pluginManager.HandleUninstallPlugin) + authRouter.HandleFunc("/api/plugins/store/list", pluginManager.HandleListDownloadablePlugins, false) + authRouter.HandleFunc("/api/plugins/store/resync", pluginManager.HandleResyncPluginList, false) + authRouter.HandleFunc("/api/plugins/store/install", pluginManager.HandleInstallPlugin, false) + authRouter.HandleFunc("/api/plugins/store/uninstall", pluginManager.HandleUninstallPlugin, false) // Developer options - authRouter.HandleFunc("/api/plugins/developer/enableAutoReload", pluginManager.HandleEnableHotReload) - authRouter.HandleFunc("/api/plugins/developer/setAutoReloadInterval", pluginManager.HandleSetHotReloadInterval) + authRouter.HandleFunc("/api/plugins/developer/enableAutoReload", pluginManager.HandleEnableHotReload, false) + authRouter.HandleFunc("/api/plugins/developer/setAutoReloadInterval", pluginManager.HandleSetHotReloadInterval, false) } // Register the APIs for Auth functions, due to scoping issue some functions are defined here @@ -372,17 +372,17 @@ func initAPIs(targetMux *http.ServeMux) { targetMux.HandleFunc("/api/account/new", HandleNewPasswordSetup) //Docker UX Optimizations - authRouter.HandleFunc("/api/docker/available", DockerUXOptimizer.HandleDockerAvailable) - authRouter.HandleFunc("/api/docker/containers", DockerUXOptimizer.HandleDockerContainersList) + authRouter.HandleFunc("/api/docker/available", DockerUXOptimizer.HandleDockerAvailable, false) + authRouter.HandleFunc("/api/docker/containers", DockerUXOptimizer.HandleDockerContainersList, false) //Others targetMux.HandleFunc("/api/info/x", HandleZoraxyInfo) - authRouter.HandleFunc("/api/info/geoip", HandleGeoIpLookup) - authRouter.HandleFunc("/api/conf/export", ExportConfigAsZip) - authRouter.HandleFunc("/api/conf/import", ImportConfigFromZip) - authRouter.HandleFunc("/api/log/list", LogViewer.HandleListLog) - authRouter.HandleFunc("/api/log/read", LogViewer.HandleReadLog) + authRouter.HandleFunc("/api/info/geoip", HandleGeoIpLookup, false) + authRouter.HandleFunc("/api/conf/export", ExportConfigAsZip, false) + authRouter.HandleFunc("/api/conf/import", ImportConfigFromZip, false) + authRouter.HandleFunc("/api/log/list", LogViewer.HandleListLog, false) + authRouter.HandleFunc("/api/log/read", LogViewer.HandleReadLog, false) //Debug - authRouter.HandleFunc("/api/info/pprof", pprof.Index) + authRouter.HandleFunc("/api/info/pprof", pprof.Index, false) } diff --git a/src/def.go b/src/def.go index 2e47323..9d550b7 100644 --- a/src/def.go +++ b/src/def.go @@ -144,6 +144,9 @@ var ( loadBalancer *loadbalance.RouteManager //Global scope loadbalancer, store the state of the lb routing pluginManager *plugins.Manager //Plugin manager for managing plugins + //Plugin auth related + pluginApiKeyManager *auth.APIKeyManager //API key manager for plugin authentication + //Authentication Provider forwardAuthRouter *forward.AuthRouter // Forward Auth router for Authelia/Authentik/etc authentication oauth2Router *oauth2.OAuth2Router //OAuth2Router router for OAuth2Router authentication diff --git a/src/mod/auth/auth.go b/src/mod/auth/auth.go index 350a972..d6ce56f 100644 --- a/src/mod/auth/auth.go +++ b/src/mod/auth/auth.go @@ -28,6 +28,8 @@ type AuthAgent struct { Database *db.Database LoginRedirectionHandler func(http.ResponseWriter, *http.Request) Logger *logger.Logger + //Plugin related + PluginAuthMiddleware *PluginAuthMiddleware //Plugin authentication middleware } type AuthEndpoints struct { @@ -39,7 +41,7 @@ type AuthEndpoints struct { } // Constructor -func NewAuthenticationAgent(sessionName string, key []byte, sysdb *db.Database, allowReg bool, systemLogger *logger.Logger, loginRedirectionHandler func(http.ResponseWriter, *http.Request)) *AuthAgent { +func NewAuthenticationAgent(sessionName string, key []byte, sysdb *db.Database, allowReg bool, systemLogger *logger.Logger, loginRedirectionHandler func(http.ResponseWriter, *http.Request), apiKeyManager *APIKeyManager) *AuthAgent { store := sessions.NewCookieStore(key) err := sysdb.NewTable("auth") if err != nil { @@ -47,6 +49,9 @@ func NewAuthenticationAgent(sessionName string, key []byte, sysdb *db.Database, panic(err) } + //Initialize the plugin authentication middleware + pluginAuthMiddleware := NewPluginAuthMiddleware(apiKeyManager) + //Create a new AuthAgent object newAuthAgent := AuthAgent{ SessionName: sessionName, @@ -54,6 +59,7 @@ func NewAuthenticationAgent(sessionName string, key []byte, sysdb *db.Database, Database: sysdb, LoginRedirectionHandler: loginRedirectionHandler, Logger: systemLogger, + PluginAuthMiddleware: pluginAuthMiddleware, } //Return the authAgent diff --git a/src/mod/auth/plugin_apikey_manager.go b/src/mod/auth/plugin_apikey_manager.go new file mode 100644 index 0000000..7a06553 --- /dev/null +++ b/src/mod/auth/plugin_apikey_manager.go @@ -0,0 +1,152 @@ +package auth + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "fmt" + "sync" + "time" + + "imuslab.com/zoraxy/mod/plugins/zoraxy_plugin" +) + +// PluginAPIKey represents an API key for a plugin +type PluginAPIKey struct { + PluginID string + APIKey string + PermittedEndpoints []zoraxy_plugin.PermittedAPIEndpoint // List of permitted API endpoints + CreatedAt time.Time +} + +// APIKeyManager manages API keys for plugins +type APIKeyManager struct { + keys map[string]*PluginAPIKey // key: API key, value: plugin info + mutex sync.RWMutex +} + +// NewAPIKeyManager creates a new API key manager +func NewAPIKeyManager() *APIKeyManager { + return &APIKeyManager{ + keys: make(map[string]*PluginAPIKey), + mutex: sync.RWMutex{}, + } +} + +// GenerateAPIKey generates a new API key for a plugin +func (m *APIKeyManager) GenerateAPIKey(pluginID string, permittedEndpoints []zoraxy_plugin.PermittedAPIEndpoint) (*PluginAPIKey, error) { + m.mutex.Lock() + defer m.mutex.Unlock() + + // Generate a cryptographically secure random key + bytes := make([]byte, 32) + if _, err := rand.Read(bytes); err != nil { + return nil, fmt.Errorf("failed to generate random bytes: %w", err) + } + + // Hash the random bytes to create the API key + hash := sha256.Sum256(bytes) + apiKey := hex.EncodeToString(hash[:]) + + // Create the plugin API key + pluginAPIKey := &PluginAPIKey{ + PluginID: pluginID, + APIKey: apiKey, + PermittedEndpoints: permittedEndpoints, + CreatedAt: time.Now(), + } + + // Store the API key + m.keys[apiKey] = pluginAPIKey + + return pluginAPIKey, nil +} + +// ValidateAPIKey validates an API key and returns the associated plugin information +func (m *APIKeyManager) ValidateAPIKey(apiKey string) (*PluginAPIKey, error) { + m.mutex.RLock() + defer m.mutex.RUnlock() + + pluginAPIKey, exists := m.keys[apiKey] + if !exists { + return nil, fmt.Errorf("invalid API key") + } + + return pluginAPIKey, nil +} + +// ValidateAPIKeyForEndpoint validates an API key for a specific endpoint +func (m *APIKeyManager) ValidateAPIKeyForEndpoint(endpoint string, method string, apiKey string) (*PluginAPIKey, error) { + pluginAPIKey, err := m.ValidateAPIKey(apiKey) + if err != nil { + return nil, err + } + + // Check if the endpoint is permitted + for _, permittedEndpoint := range pluginAPIKey.PermittedEndpoints { + if permittedEndpoint.Endpoint == endpoint { + return pluginAPIKey, nil + } + } + + return nil, fmt.Errorf("endpoint not permitted for this API key") +} + +// RevokeAPIKey revokes an API key +func (m *APIKeyManager) RevokeAPIKey(apiKey string) error { + m.mutex.Lock() + defer m.mutex.Unlock() + + if _, exists := m.keys[apiKey]; !exists { + return fmt.Errorf("API key not found") + } + + delete(m.keys, apiKey) + return nil +} + +// RevokeAPIKeysForPlugin revokes all API keys for a specific plugin +func (m *APIKeyManager) RevokeAPIKeysForPlugin(pluginID string) error { + m.mutex.Lock() + defer m.mutex.Unlock() + + keysToRemove := []string{} + for apiKey, pluginAPIKey := range m.keys { + if pluginAPIKey.PluginID == pluginID { + keysToRemove = append(keysToRemove, apiKey) + } + } + + for _, apiKey := range keysToRemove { + delete(m.keys, apiKey) + } + + return nil +} + +// GetAPIKeyForPlugin returns the API key for a plugin (if exists) +func (m *APIKeyManager) GetAPIKeyForPlugin(pluginID string) (*PluginAPIKey, error) { + m.mutex.RLock() + defer m.mutex.RUnlock() + + for _, pluginAPIKey := range m.keys { + if pluginAPIKey.PluginID == pluginID { + return pluginAPIKey, nil + } + } + + return nil, fmt.Errorf("no API key found for plugin") +} + +// ListAPIKeys returns all API keys (for debugging purposes) +func (m *APIKeyManager) ListAPIKeys() []*PluginAPIKey { + m.mutex.RLock() + defer m.mutex.RUnlock() + + keys := make([]*PluginAPIKey, 0, len(m.keys)) + for _, pluginAPIKey := range m.keys { + keys = append(keys, pluginAPIKey) + } + + return keys +} diff --git a/src/mod/auth/plugin_middleware.go b/src/mod/auth/plugin_middleware.go new file mode 100644 index 0000000..9be7cb8 --- /dev/null +++ b/src/mod/auth/plugin_middleware.go @@ -0,0 +1,77 @@ +// Handles the API-Key based authentication for plugins + +package auth + +import ( + "net/http" + "strings" +) + +// PluginAuthMiddleware provides authentication middleware for plugin API requests +type PluginAuthMiddleware struct { + apiKeyManager *APIKeyManager +} + +// NewPluginAuthMiddleware creates a new plugin authentication middleware +func NewPluginAuthMiddleware(apiKeyManager *APIKeyManager) *PluginAuthMiddleware { + return &PluginAuthMiddleware{ + apiKeyManager: apiKeyManager, + } +} + +// ValidatePluginAPIRequest validates an API request with plugin API key for a specific endpoint +func (m *PluginAuthMiddleware) ValidatePluginAPIRequest(endpoint string, method string, apiKey string) (*PluginAPIKey, error) { + return m.apiKeyManager.ValidateAPIKeyForEndpoint(endpoint, method, apiKey) +} + +// WrapHandler wraps an HTTP handler with plugin authentication middleware +func (m *PluginAuthMiddleware) WrapHandler(endpoint string, handler http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // First, remove any existing plugin authentication headers + r.Header.Del("X-Zoraxy-Plugin-ID") + r.Header.Del("X-Zoraxy-Plugin-Auth") + + // Check for API key in the Authorization header + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + // No authorization header, proceed with normal authentication + handler(w, r) + return + } + + // Check if it's a plugin API key (Bearer token) + if !strings.HasPrefix(authHeader, "Bearer ") { + // Not a Bearer token, proceed with normal authentication + handler(w, r) + return + } + + // Extract the API key + apiKey := strings.TrimPrefix(authHeader, "Bearer ") + + // Validate the API key for this endpoint + pluginAPIKey, err := m.ValidatePluginAPIRequest(endpoint, r.Method, apiKey) + if err != nil { + // Invalid API key or endpoint not permitted + http.Error(w, "Unauthorized: Invalid API key or endpoint not permitted", http.StatusUnauthorized) + return + } + + // Add plugin information to the request context + r.Header.Set("X-Zoraxy-Plugin-ID", pluginAPIKey.PluginID) + r.Header.Set("X-Zoraxy-Plugin-Auth", "true") + + // Call the original handler + handler(w, r) + } +} + +// GetPluginIDFromRequest extracts the plugin ID from the request if authenticated via plugin API key +func GetPluginIDFromRequest(r *http.Request) string { + return r.Header.Get("X-Plugin-ID") +} + +// IsPluginAuthenticated checks if the request is authenticated via plugin API key +func IsPluginAuthenticated(r *http.Request) bool { + return r.Header.Get("X-Plugin-Auth") == "true" +} diff --git a/src/mod/auth/router.go b/src/mod/auth/router.go index 85f9dae..572df8e 100644 --- a/src/mod/auth/router.go +++ b/src/mod/auth/router.go @@ -25,7 +25,7 @@ func NewManagedHTTPRouter(option RouterOption) *RouterDef { } } -func (router *RouterDef) HandleFunc(endpoint string, handler func(http.ResponseWriter, *http.Request)) error { +func (router *RouterDef) HandleFunc(endpoint string, handler func(http.ResponseWriter, *http.Request), pluginAccessible bool) error { //Check if the endpoint already registered if _, exist := router.endpoints[endpoint]; exist { fmt.Println("WARNING! Duplicated registering of web endpoint: " + endpoint) @@ -34,31 +34,28 @@ func (router *RouterDef) HandleFunc(endpoint string, handler func(http.ResponseW authAgent := router.option.AuthAgent + authWrapper := func(w http.ResponseWriter, r *http.Request) { + //Check authentication of the user + X_Plugin_Auth := r.Header.Get("X-Zoraxy-Plugin-Auth") + if router.option.RequireAuth && !(pluginAccessible && X_Plugin_Auth == "true") { + authAgent.HandleCheckAuth(w, r, func(w http.ResponseWriter, r *http.Request) { + handler(w, r) + }) + } else { + handler(w, r) + } + } + + // if the endpoint is supposed to be plugin accessible, wrap it with plugin authentication middleware + if pluginAccessible { + authWrapper = router.option.AuthAgent.PluginAuthMiddleware.WrapHandler(endpoint, authWrapper) + } + //OK. Register handler if router.option.TargetMux == nil { - http.HandleFunc(endpoint, func(w http.ResponseWriter, r *http.Request) { - //Check authentication of the user - if router.option.RequireAuth { - authAgent.HandleCheckAuth(w, r, func(w http.ResponseWriter, r *http.Request) { - handler(w, r) - }) - } else { - handler(w, r) - } - - }) + http.HandleFunc(endpoint, authWrapper) } else { - router.option.TargetMux.HandleFunc(endpoint, func(w http.ResponseWriter, r *http.Request) { - //Check authentication of the user - if router.option.RequireAuth { - authAgent.HandleCheckAuth(w, r, func(w http.ResponseWriter, r *http.Request) { - handler(w, r) - }) - } else { - handler(w, r) - } - - }) + router.option.TargetMux.HandleFunc(endpoint, authWrapper) } router.endpoints[endpoint] = handler diff --git a/src/mod/plugins/lifecycle.go b/src/mod/plugins/lifecycle.go index 914acf7..cceff11 100644 --- a/src/mod/plugins/lifecycle.go +++ b/src/mod/plugins/lifecycle.go @@ -41,6 +41,18 @@ func (m *Manager) StartPlugin(pluginID string) error { Port: getRandomPortNumber(), RuntimeConst: *m.Options.SystemConst, } + + // Generate API key if the plugin has permitted endpoints + if len(thisPlugin.Spec.PermittedAPIEndpoints) > 0 { + apiKey, err := m.Options.APIKeyManager.GenerateAPIKey(thisPlugin.Spec.ID, thisPlugin.Spec.PermittedAPIEndpoints) + if err != nil { + return err + } + pluginConfiguration.APIKey = apiKey.APIKey + pluginConfiguration.ZoraxyPort = m.Options.ZoraxyPort + m.Log("Generated API key for plugin "+thisPlugin.Spec.Name, nil) + } + js, _ := json.Marshal(pluginConfiguration) //Start the plugin with given configuration @@ -270,6 +282,13 @@ func (m *Manager) StopPlugin(pluginID string) error { thisPlugin.Enabled = false thisPlugin.StopAllStaticPathRouters() thisPlugin.StopDynamicForwardRouter() + + //Clean up API key + err = m.Options.APIKeyManager.RevokeAPIKeysForPlugin(thisPlugin.Spec.ID) + if err != nil { + m.Log("Failed to revoke API keys for plugin "+thisPlugin.Spec.Name, err) + } + return nil } diff --git a/src/mod/plugins/typdef.go b/src/mod/plugins/typdef.go index 6b64068..befdf97 100644 --- a/src/mod/plugins/typdef.go +++ b/src/mod/plugins/typdef.go @@ -7,6 +7,7 @@ import ( "sync" "time" + "imuslab.com/zoraxy/mod/auth" "imuslab.com/zoraxy/mod/database" "imuslab.com/zoraxy/mod/dynamicproxy/dpcore" "imuslab.com/zoraxy/mod/info/logger" @@ -42,10 +43,14 @@ type ManagerOptions struct { /* Runtime */ SystemConst *zoraxyPlugin.RuntimeConstantValue //The system constant value + ZoraxyPort int //The port of the Zoraxy instance, used for API calls CSRFTokenGen func(*http.Request) string `json:"-"` //The CSRF token generator function Database *database.Database `json:"-"` Logger *logger.Logger `json:"-"` + /* API Key Management */ + APIKeyManager *auth.APIKeyManager `json:"-"` //The API key manager for the plugins + /* Development */ EnableHotReload bool //Check if the plugin file is changed and reload the plugin automatically HotReloadInterval int //The interval for checking the plugin file change, in seconds diff --git a/src/mod/plugins/zoraxy_plugin/zoraxy_plugin.go b/src/mod/plugins/zoraxy_plugin/zoraxy_plugin.go index 737e928..5398087 100644 --- a/src/mod/plugins/zoraxy_plugin/zoraxy_plugin.go +++ b/src/mod/plugins/zoraxy_plugin/zoraxy_plugin.go @@ -47,6 +47,12 @@ type RuntimeConstantValue struct { DevelopmentBuild bool `json:"development_build"` //Whether the Zoraxy is a development build or not } +type PermittedAPIEndpoint struct { + Method string `json:"method"` //HTTP method for the API endpoint (e.g., GET, POST) + Endpoint string `json:"endpoint"` //The API endpoint that the plugin can access + Reason string `json:"reason"` //The reason why the plugin needs to access this endpoint +} + /* IntroSpect Payload @@ -97,7 +103,10 @@ type IntroSpect struct { /* Subscriptions Settings */ SubscriptionPath string `json:"subscription_path"` //Subscription event path of your plugin (e.g. /notifyme), a POST request with SubscriptionEvent as body will be sent to this path when the event is triggered - SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, see Zoraxy documentation for more details + SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, paired with comments describing how the event is used, see Zoraxy documentation for more details + + /* API Access Control */ + PermittedAPIEndpoints []PermittedAPIEndpoint `json:"permitted_api_endpoints"` //List of API endpoints this plugin can access, and a description of why the plugin needs to access this endpoint } /* @@ -126,8 +135,10 @@ by the supplied values like starting a web server at given port that listens to 127.0.0.1:port */ type ConfigureSpec struct { - Port int `json:"port"` //Port to listen - RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values + Port int `json:"port"` //Port to listen + RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values + APIKey string `json:"api_key,omitempty"` //API key for accessing Zoraxy APIs, if the plugin has permitted endpoints + ZoraxyPort int `json:"zoraxy_port,omitempty"` //The port that Zoraxy is running on, used for making API calls to Zoraxy //To be expanded } diff --git a/src/start.go b/src/start.go index 359da3a..1cc55bf 100644 --- a/src/start.go +++ b/src/start.go @@ -3,6 +3,7 @@ package main import ( "log" "net/http" + "net/netip" "os" "runtime" "strconv" @@ -90,6 +91,7 @@ func startupSequence() { os.MkdirAll(CONF_HTTP_PROXY, 0775) //Create an auth agent + pluginApiKeyManager = auth.NewAPIKeyManager() sessionKey, err := auth.GetSessionKey(sysdb, SystemWideLogger) if err != nil { log.Fatal(err) @@ -97,7 +99,7 @@ func startupSequence() { authAgent = auth.NewAuthenticationAgent(SYSTEM_NAME, []byte(sessionKey), sysdb, true, SystemWideLogger, func(w http.ResponseWriter, r *http.Request) { //Not logged in. Redirecting to login page http.Redirect(w, r, "/login.html", http.StatusTemporaryRedirect) - }) + }, pluginApiKeyManager) //Create a TLS certificate manager tlsCertManager, err = tlscert.NewManager(CONF_CERT_STORE, SystemWideLogger) @@ -313,11 +315,18 @@ func startupSequence() { */ pluginFolder := *path_plugin pluginFolder = strings.TrimSuffix(pluginFolder, "/") + ZoraxyAddrPort, err := netip.ParseAddrPort(*webUIPort) + ZoraxyPort := 8000 + if err == nil && ZoraxyAddrPort.IsValid() && ZoraxyAddrPort.Port() > 0 { + ZoraxyPort = int(ZoraxyAddrPort.Port()) + } pluginManager = plugins.NewPluginManager(&plugins.ManagerOptions{ PluginDir: pluginFolder, Database: sysdb, Logger: SystemWideLogger, PluginGroupsConfig: CONF_PLUGIN_GROUPS, + APIKeyManager: pluginApiKeyManager, + ZoraxyPort: ZoraxyPort, CSRFTokenGen: func(r *http.Request) string { return csrf.Token(r) }, From 5a38c1d4078ae0b3e6146ba336a18c7132b32b6d Mon Sep 17 00:00:00 2001 From: Anthony Rubick <68485672+AnthonyMichaelTDM@users.noreply.github.com> Date: Thu, 17 Jul 2025 20:20:24 -0700 Subject: [PATCH 02/12] feat(plugins): Add API Call Example Plugin --- example/plugins/api-call-example/go.mod | 3 + example/plugins/api-call-example/main.go | 54 +++++ .../mod/zoraxy_plugin/README.txt | 19 ++ .../mod/zoraxy_plugin/dev_webserver.go | 145 ++++++++++++ .../mod/zoraxy_plugin/dynamic_router.go | 162 +++++++++++++ .../mod/zoraxy_plugin/embed_webserver.go | 156 ++++++++++++ .../mod/zoraxy_plugin/static_router.go | 105 +++++++++ .../mod/zoraxy_plugin/zoraxy_plugin.go | 187 +++++++++++++++ example/plugins/api-call-example/ui.go | 223 ++++++++++++++++++ 9 files changed, 1054 insertions(+) create mode 100644 example/plugins/api-call-example/go.mod create mode 100644 example/plugins/api-call-example/main.go create mode 100644 example/plugins/api-call-example/mod/zoraxy_plugin/README.txt create mode 100644 example/plugins/api-call-example/mod/zoraxy_plugin/dev_webserver.go create mode 100644 example/plugins/api-call-example/mod/zoraxy_plugin/dynamic_router.go create mode 100644 example/plugins/api-call-example/mod/zoraxy_plugin/embed_webserver.go create mode 100644 example/plugins/api-call-example/mod/zoraxy_plugin/static_router.go create mode 100644 example/plugins/api-call-example/mod/zoraxy_plugin/zoraxy_plugin.go create mode 100644 example/plugins/api-call-example/ui.go diff --git a/example/plugins/api-call-example/go.mod b/example/plugins/api-call-example/go.mod new file mode 100644 index 0000000..6b83139 --- /dev/null +++ b/example/plugins/api-call-example/go.mod @@ -0,0 +1,3 @@ +module aroz.org/zoraxy/api-call-example + +go 1.24.5 \ No newline at end of file diff --git a/example/plugins/api-call-example/main.go b/example/plugins/api-call-example/main.go new file mode 100644 index 0000000..bf78cac --- /dev/null +++ b/example/plugins/api-call-example/main.go @@ -0,0 +1,54 @@ +package main + +import ( + "fmt" + "net/http" + + plugin "aroz.org/zoraxy/api-call-example/mod/zoraxy_plugin" +) + +const ( + PLUGIN_ID = "org.aroz.zoraxy.api_call_example" + UI_PATH = "/ui" +) + +func main() { + // Serve the plugin intro spect + // This will print the plugin intro spect and exit if the -introspect flag is provided + runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{ + ID: PLUGIN_ID, + Name: "API Call Example Plugin", + Author: "Anthony Rubick", + AuthorContact: "", + Description: "An example plugin for making API calls", + Type: plugin.PluginType_Utilities, + VersionMajor: 1, + VersionMinor: 0, + VersionPatch: 0, + + UIPath: UI_PATH, + + /* API Access Control */ + PermittedAPIEndpoints: []plugin.PermittedAPIEndpoint{ + { + Method: http.MethodGet, + Endpoint: "/api/access/list", + Reason: "Used to display all configured Access Rules", + }, + }, + }) + + if err != nil { + fmt.Printf("Error serving introspect: %v\n", err) + return + } + + // Start the HTTP server + http.HandleFunc(UI_PATH+"/", func(w http.ResponseWriter, r *http.Request) { + RenderUI(runtimeCfg, w, r) + }) + + serverAddr := fmt.Sprintf("127.0.0.1:%d", runtimeCfg.Port) + fmt.Printf("Starting API Call Example Plugin on %s\n", serverAddr) + http.ListenAndServe(serverAddr, nil) +} diff --git a/example/plugins/api-call-example/mod/zoraxy_plugin/README.txt b/example/plugins/api-call-example/mod/zoraxy_plugin/README.txt new file mode 100644 index 0000000..ed8a405 --- /dev/null +++ b/example/plugins/api-call-example/mod/zoraxy_plugin/README.txt @@ -0,0 +1,19 @@ +# Zoraxy Plugin + +## Overview +This module serves as a template for building your own plugins for the Zoraxy Reverse Proxy. By copying this module to your plugin mod folder, you can create a new plugin with the necessary structure and components. + +## Instructions + +1. **Copy the Module:** + - Copy the entire `zoraxy_plugin` module to your plugin mod folder. + +2. **Include the Structure:** + - Ensure that you maintain the directory structure and file organization as provided in this module. + +3. **Modify as Needed:** + - Customize the copied module to implement the desired functionality for your plugin. + +## Directory Structure + zoraxy_plugin: Handle -introspect and -configuration process required for plugin loading and startup + embed_webserver: Handle embeded web server routing and injecting csrf token to your plugin served UI pages \ No newline at end of file diff --git a/example/plugins/api-call-example/mod/zoraxy_plugin/dev_webserver.go b/example/plugins/api-call-example/mod/zoraxy_plugin/dev_webserver.go new file mode 100644 index 0000000..9bed106 --- /dev/null +++ b/example/plugins/api-call-example/mod/zoraxy_plugin/dev_webserver.go @@ -0,0 +1,145 @@ +package zoraxy_plugin + +import ( + "fmt" + "net/http" + "os" + "strings" + "time" +) + +type PluginUiDebugRouter struct { + PluginID string //The ID of the plugin + TargetDir string //The directory where the UI files are stored + HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui + EnableDebug bool //Enable debug mode + terminateHandler func() //The handler to be called when the plugin is terminated +} + +// NewPluginFileSystemUIRouter creates a new PluginUiRouter with file system +// The targetDir is the directory where the UI files are stored (e.g. ./www) +// The handlerPrefix is the prefix of the handler used to route this router +// The handlerPrefix should start with a slash (e.g. /ui) that matches the http.Handle path +// All prefix should not end with a slash +func NewPluginFileSystemUIRouter(pluginID string, targetDir string, handlerPrefix string) *PluginUiDebugRouter { + //Make sure all prefix are in /prefix format + if !strings.HasPrefix(handlerPrefix, "/") { + handlerPrefix = "/" + handlerPrefix + } + handlerPrefix = strings.TrimSuffix(handlerPrefix, "/") + + //Return the PluginUiRouter + return &PluginUiDebugRouter{ + PluginID: pluginID, + TargetDir: targetDir, + HandlerPrefix: handlerPrefix, + } +} + +func (p *PluginUiDebugRouter) populateCSRFToken(r *http.Request, fsHandler http.Handler) http.Handler { + //Get the CSRF token from header + csrfToken := r.Header.Get("X-Zoraxy-Csrf") + if csrfToken == "" { + csrfToken = "missing-csrf-token" + } + + //Return the middleware + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check if the request is for an HTML file + if strings.HasSuffix(r.URL.Path, ".html") { + //Read the target file from file system + targetFilePath := strings.TrimPrefix(r.URL.Path, "/") + targetFilePath = p.TargetDir + "/" + targetFilePath + targetFilePath = strings.TrimPrefix(targetFilePath, "/") + targetFileContent, err := os.ReadFile(targetFilePath) + if err != nil { + http.Error(w, "File not found", http.StatusNotFound) + return + } + body := string(targetFileContent) + body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken) + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(http.StatusOK) + w.Write([]byte(body)) + return + } else if strings.HasSuffix(r.URL.Path, "/") { + //Check if the request is for a directory + //Check if the directory has an index.html file + targetFilePath := strings.TrimPrefix(r.URL.Path, "/") + targetFilePath = p.TargetDir + "/" + targetFilePath + "index.html" + targetFilePath = strings.TrimPrefix(targetFilePath, "/") + if _, err := os.Stat(targetFilePath); err == nil { + //Serve the index.html file + targetFileContent, err := os.ReadFile(targetFilePath) + if err != nil { + http.Error(w, "File not found", http.StatusNotFound) + return + } + body := string(targetFileContent) + body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken) + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(http.StatusOK) + w.Write([]byte(body)) + return + } + } + + //Call the next handler + fsHandler.ServeHTTP(w, r) + }) + +} + +// GetHttpHandler returns the http.Handler for the PluginUiRouter +func (p *PluginUiDebugRouter) Handler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + //Remove the plugin UI handler path prefix + if p.EnableDebug { + fmt.Print("Request URL:", r.URL.Path, " rewriting to ") + } + + rewrittenURL := r.RequestURI + rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix) + rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/") + r.URL.Path = rewrittenURL + r.RequestURI = rewrittenURL + if p.EnableDebug { + fmt.Println(r.URL.Path) + } + + //Serve the file from the file system + fsHandler := http.FileServer(http.Dir(p.TargetDir)) + + // Replace {{csrf_token}} with the actual CSRF token and serve the file + p.populateCSRFToken(r, fsHandler).ServeHTTP(w, r) + }) +} + +// RegisterTerminateHandler registers the terminate handler for the PluginUiRouter +// The terminate handler will be called when the plugin is terminated from Zoraxy plugin manager +// if mux is nil, the handler will be registered to http.DefaultServeMux +func (p *PluginUiDebugRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) { + p.terminateHandler = termFunc + if mux == nil { + mux = http.DefaultServeMux + } + mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) { + p.terminateHandler() + w.WriteHeader(http.StatusOK) + go func() { + //Make sure the response is sent before the plugin is terminated + time.Sleep(100 * time.Millisecond) + os.Exit(0) + }() + }) +} + +// Attach the file system UI handler to the target http.ServeMux +func (p *PluginUiDebugRouter) AttachHandlerToMux(mux *http.ServeMux) { + if mux == nil { + mux = http.DefaultServeMux + } + + p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/") + mux.Handle(p.HandlerPrefix+"/", p.Handler()) +} diff --git a/example/plugins/api-call-example/mod/zoraxy_plugin/dynamic_router.go b/example/plugins/api-call-example/mod/zoraxy_plugin/dynamic_router.go new file mode 100644 index 0000000..22e56be --- /dev/null +++ b/example/plugins/api-call-example/mod/zoraxy_plugin/dynamic_router.go @@ -0,0 +1,162 @@ +package zoraxy_plugin + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" +) + +/* + + Dynamic Path Handler + +*/ + +type SniffResult int + +const ( + SniffResultAccept SniffResult = iota // Forward the request to this plugin dynamic capture ingress + SniffResultSkip // Skip this plugin and let the next plugin handle the request +) + +type SniffHandler func(*DynamicSniffForwardRequest) SniffResult + +/* +RegisterDynamicSniffHandler registers a dynamic sniff handler for a path +You can decide to accept or skip the request based on the request header and paths +*/ +func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http.ServeMux, handler SniffHandler) { + if !strings.HasSuffix(sniff_ingress, "/") { + sniff_ingress = sniff_ingress + "/" + } + mux.Handle(sniff_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if p.enableDebugPrint { + fmt.Println("Request captured by dynamic sniff path: " + r.RequestURI) + } + + // Decode the request payload + jsonBytes, err := io.ReadAll(r.Body) + if err != nil { + if p.enableDebugPrint { + fmt.Println("Error reading request body:", err) + } + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + payload, err := DecodeForwardRequestPayload(jsonBytes) + if err != nil { + if p.enableDebugPrint { + fmt.Println("Error decoding request payload:", err) + fmt.Print("Payload: ") + fmt.Println(string(jsonBytes)) + } + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + // Get the forwarded request UUID + forwardUUID := r.Header.Get("X-Zoraxy-RequestID") + payload.requestUUID = forwardUUID + payload.rawRequest = r + + sniffResult := handler(&payload) + if sniffResult == SniffResultAccept { + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) + } else { + w.WriteHeader(http.StatusNotImplemented) + w.Write([]byte("SKIP")) + } + })) +} + +// RegisterDynamicCaptureHandle register the dynamic capture ingress path with a handler +func (p *PathRouter) RegisterDynamicCaptureHandle(capture_ingress string, mux *http.ServeMux, handlefunc func(http.ResponseWriter, *http.Request)) { + if !strings.HasSuffix(capture_ingress, "/") { + capture_ingress = capture_ingress + "/" + } + mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if p.enableDebugPrint { + fmt.Println("Request captured by dynamic capture path: " + r.RequestURI) + } + + rewrittenURL := r.RequestURI + rewrittenURL = strings.TrimPrefix(rewrittenURL, capture_ingress) + rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/") + if rewrittenURL == "" { + rewrittenURL = "/" + } + if !strings.HasPrefix(rewrittenURL, "/") { + rewrittenURL = "/" + rewrittenURL + } + r.RequestURI = rewrittenURL + + handlefunc(w, r) + })) +} + +/* + Sniffing and forwarding + + The following functions are here to help with + sniffing and forwarding requests to the dynamic + router. +*/ +// A custom request object to be used in the dynamic sniffing +type DynamicSniffForwardRequest struct { + Method string `json:"method"` + Hostname string `json:"hostname"` + URL string `json:"url"` + Header map[string][]string `json:"header"` + RemoteAddr string `json:"remote_addr"` + Host string `json:"host"` + RequestURI string `json:"request_uri"` + Proto string `json:"proto"` + ProtoMajor int `json:"proto_major"` + ProtoMinor int `json:"proto_minor"` + + /* Internal use */ + rawRequest *http.Request `json:"-"` + requestUUID string `json:"-"` +} + +// GetForwardRequestPayload returns a DynamicSniffForwardRequest object from an http.Request object +func EncodeForwardRequestPayload(r *http.Request) DynamicSniffForwardRequest { + return DynamicSniffForwardRequest{ + Method: r.Method, + Hostname: r.Host, + URL: r.URL.String(), + Header: r.Header, + RemoteAddr: r.RemoteAddr, + Host: r.Host, + RequestURI: r.RequestURI, + Proto: r.Proto, + ProtoMajor: r.ProtoMajor, + ProtoMinor: r.ProtoMinor, + rawRequest: r, + } +} + +// DecodeForwardRequestPayload decodes JSON bytes into a DynamicSniffForwardRequest object +func DecodeForwardRequestPayload(jsonBytes []byte) (DynamicSniffForwardRequest, error) { + var payload DynamicSniffForwardRequest + err := json.Unmarshal(jsonBytes, &payload) + if err != nil { + return DynamicSniffForwardRequest{}, err + } + return payload, nil +} + +// GetRequest returns the original http.Request object, for debugging purposes +func (dsfr *DynamicSniffForwardRequest) GetRequest() *http.Request { + return dsfr.rawRequest +} + +// GetRequestUUID returns the request UUID +// if this UUID is empty string, that might indicate the request +// is not coming from the dynamic router +func (dsfr *DynamicSniffForwardRequest) GetRequestUUID() string { + return dsfr.requestUUID +} diff --git a/example/plugins/api-call-example/mod/zoraxy_plugin/embed_webserver.go b/example/plugins/api-call-example/mod/zoraxy_plugin/embed_webserver.go new file mode 100644 index 0000000..b64318f --- /dev/null +++ b/example/plugins/api-call-example/mod/zoraxy_plugin/embed_webserver.go @@ -0,0 +1,156 @@ +package zoraxy_plugin + +import ( + "embed" + "fmt" + "io/fs" + "net/http" + "net/url" + "os" + "strings" + "time" +) + +type PluginUiRouter struct { + PluginID string //The ID of the plugin + TargetFs *embed.FS //The embed.FS where the UI files are stored + TargetFsPrefix string //The prefix of the embed.FS where the UI files are stored, e.g. /web + HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui + EnableDebug bool //Enable debug mode + terminateHandler func() //The handler to be called when the plugin is terminated +} + +// NewPluginEmbedUIRouter creates a new PluginUiRouter with embed.FS +// The targetFsPrefix is the prefix of the embed.FS where the UI files are stored +// The targetFsPrefix should be relative to the root of the embed.FS +// The targetFsPrefix should start with a slash (e.g. /web) that corresponds to the root folder of the embed.FS +// The handlerPrefix is the prefix of the handler used to route this router +// The handlerPrefix should start with a slash (e.g. /ui) that matches the http.Handle path +// All prefix should not end with a slash +func NewPluginEmbedUIRouter(pluginID string, targetFs *embed.FS, targetFsPrefix string, handlerPrefix string) *PluginUiRouter { + //Make sure all prefix are in /prefix format + if !strings.HasPrefix(targetFsPrefix, "/") { + targetFsPrefix = "/" + targetFsPrefix + } + targetFsPrefix = strings.TrimSuffix(targetFsPrefix, "/") + + if !strings.HasPrefix(handlerPrefix, "/") { + handlerPrefix = "/" + handlerPrefix + } + handlerPrefix = strings.TrimSuffix(handlerPrefix, "/") + + //Return the PluginUiRouter + return &PluginUiRouter{ + PluginID: pluginID, + TargetFs: targetFs, + TargetFsPrefix: targetFsPrefix, + HandlerPrefix: handlerPrefix, + } +} + +func (p *PluginUiRouter) populateCSRFToken(r *http.Request, fsHandler http.Handler) http.Handler { + //Get the CSRF token from header + csrfToken := r.Header.Get("X-Zoraxy-Csrf") + if csrfToken == "" { + csrfToken = "missing-csrf-token" + } + + //Return the middleware + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check if the request is for an HTML file + if strings.HasSuffix(r.URL.Path, ".html") { + //Read the target file from embed.FS + targetFilePath := strings.TrimPrefix(r.URL.Path, "/") + targetFilePath = p.TargetFsPrefix + "/" + targetFilePath + targetFilePath = strings.TrimPrefix(targetFilePath, "/") + targetFileContent, err := fs.ReadFile(*p.TargetFs, targetFilePath) + if err != nil { + http.Error(w, "File not found", http.StatusNotFound) + return + } + body := string(targetFileContent) + body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken) + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(http.StatusOK) + w.Write([]byte(body)) + return + } else if strings.HasSuffix(r.URL.Path, "/") { + // Check if the directory has an index.html file + indexFilePath := strings.TrimPrefix(r.URL.Path, "/") + "index.html" + indexFilePath = p.TargetFsPrefix + "/" + indexFilePath + indexFilePath = strings.TrimPrefix(indexFilePath, "/") + indexFileContent, err := fs.ReadFile(*p.TargetFs, indexFilePath) + if err == nil { + body := string(indexFileContent) + body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken) + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(http.StatusOK) + w.Write([]byte(body)) + return + } + } + + //Call the next handler + fsHandler.ServeHTTP(w, r) + }) + +} + +// GetHttpHandler returns the http.Handler for the PluginUiRouter +func (p *PluginUiRouter) Handler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + //Remove the plugin UI handler path prefix + if p.EnableDebug { + fmt.Print("Request URL:", r.URL.Path, " rewriting to ") + } + + rewrittenURL := r.RequestURI + rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix) + rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/") + r.URL, _ = url.Parse(rewrittenURL) + r.RequestURI = rewrittenURL + if p.EnableDebug { + fmt.Println(r.URL.Path) + } + + //Serve the file from the embed.FS + subFS, err := fs.Sub(*p.TargetFs, strings.TrimPrefix(p.TargetFsPrefix, "/")) + if err != nil { + fmt.Println(err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + // Replace {{csrf_token}} with the actual CSRF token and serve the file + p.populateCSRFToken(r, http.FileServer(http.FS(subFS))).ServeHTTP(w, r) + }) +} + +// RegisterTerminateHandler registers the terminate handler for the PluginUiRouter +// The terminate handler will be called when the plugin is terminated from Zoraxy plugin manager +// if mux is nil, the handler will be registered to http.DefaultServeMux +func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) { + p.terminateHandler = termFunc + if mux == nil { + mux = http.DefaultServeMux + } + mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) { + p.terminateHandler() + w.WriteHeader(http.StatusOK) + go func() { + //Make sure the response is sent before the plugin is terminated + time.Sleep(100 * time.Millisecond) + os.Exit(0) + }() + }) +} + +// Attach the embed UI handler to the target http.ServeMux +func (p *PluginUiRouter) AttachHandlerToMux(mux *http.ServeMux) { + if mux == nil { + mux = http.DefaultServeMux + } + + p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/") + mux.Handle(p.HandlerPrefix+"/", p.Handler()) +} diff --git a/example/plugins/api-call-example/mod/zoraxy_plugin/static_router.go b/example/plugins/api-call-example/mod/zoraxy_plugin/static_router.go new file mode 100644 index 0000000..f4abcb7 --- /dev/null +++ b/example/plugins/api-call-example/mod/zoraxy_plugin/static_router.go @@ -0,0 +1,105 @@ +package zoraxy_plugin + +import ( + "fmt" + "net/http" + "sort" + "strings" +) + +type PathRouter struct { + enableDebugPrint bool + pathHandlers map[string]http.Handler + defaultHandler http.Handler +} + +// NewPathRouter creates a new PathRouter +func NewPathRouter() *PathRouter { + return &PathRouter{ + enableDebugPrint: false, + pathHandlers: make(map[string]http.Handler), + } +} + +// RegisterPathHandler registers a handler for a path +func (p *PathRouter) RegisterPathHandler(path string, handler http.Handler) { + path = strings.TrimSuffix(path, "/") + p.pathHandlers[path] = handler +} + +// RemovePathHandler removes a handler for a path +func (p *PathRouter) RemovePathHandler(path string) { + delete(p.pathHandlers, path) +} + +// SetDefaultHandler sets the default handler for the router +// This handler will be called if no path handler is found +func (p *PathRouter) SetDefaultHandler(handler http.Handler) { + p.defaultHandler = handler +} + +// SetDebugPrintMode sets the debug print mode +func (p *PathRouter) SetDebugPrintMode(enable bool) { + p.enableDebugPrint = enable +} + +// StartStaticCapture starts the static capture ingress +func (p *PathRouter) RegisterStaticCaptureHandle(capture_ingress string, mux *http.ServeMux) { + if !strings.HasSuffix(capture_ingress, "/") { + capture_ingress = capture_ingress + "/" + } + mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + p.staticCaptureServeHTTP(w, r) + })) +} + +// staticCaptureServeHTTP serves the static capture path using user defined handler +func (p *PathRouter) staticCaptureServeHTTP(w http.ResponseWriter, r *http.Request) { + capturePath := r.Header.Get("X-Zoraxy-Capture") + if capturePath != "" { + if p.enableDebugPrint { + fmt.Printf("Using capture path: %s\n", capturePath) + } + originalURI := r.Header.Get("X-Zoraxy-Uri") + r.URL.Path = originalURI + if handler, ok := p.pathHandlers[capturePath]; ok { + handler.ServeHTTP(w, r) + return + } + } + p.defaultHandler.ServeHTTP(w, r) +} + +func (p *PathRouter) PrintRequestDebugMessage(r *http.Request) { + if p.enableDebugPrint { + fmt.Printf("Capture Request with path: %s \n\n**Request Headers** \n\n", r.URL.Path) + keys := make([]string, 0, len(r.Header)) + for key := range r.Header { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + for _, value := range r.Header[key] { + fmt.Printf("%s: %s\n", key, value) + } + } + + fmt.Printf("\n\n**Request Details**\n\n") + fmt.Printf("Method: %s\n", r.Method) + fmt.Printf("URL: %s\n", r.URL.String()) + fmt.Printf("Proto: %s\n", r.Proto) + fmt.Printf("Host: %s\n", r.Host) + fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr) + fmt.Printf("RequestURI: %s\n", r.RequestURI) + fmt.Printf("ContentLength: %d\n", r.ContentLength) + fmt.Printf("TransferEncoding: %v\n", r.TransferEncoding) + fmt.Printf("Close: %v\n", r.Close) + fmt.Printf("Form: %v\n", r.Form) + fmt.Printf("PostForm: %v\n", r.PostForm) + fmt.Printf("MultipartForm: %v\n", r.MultipartForm) + fmt.Printf("Trailer: %v\n", r.Trailer) + fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr) + fmt.Printf("RequestURI: %s\n", r.RequestURI) + + } +} diff --git a/example/plugins/api-call-example/mod/zoraxy_plugin/zoraxy_plugin.go b/example/plugins/api-call-example/mod/zoraxy_plugin/zoraxy_plugin.go new file mode 100644 index 0000000..5398087 --- /dev/null +++ b/example/plugins/api-call-example/mod/zoraxy_plugin/zoraxy_plugin.go @@ -0,0 +1,187 @@ +package zoraxy_plugin + +import ( + "encoding/json" + "fmt" + "os" + "strings" +) + +/* + Plugins Includes.go + + This file is copied from Zoraxy source code + You can always find the latest version under mod/plugins/includes.go + Usually this file are backward compatible +*/ + +type PluginType int + +const ( + PluginType_Router PluginType = 0 //Router Plugin, used for handling / routing / forwarding traffic + PluginType_Utilities PluginType = 1 //Utilities Plugin, used for utilities like Zerotier or Static Web Server that do not require interception with the dpcore +) + +type StaticCaptureRule struct { + CapturePath string `json:"capture_path"` + //To be expanded +} + +type ControlStatusCode int + +const ( + ControlStatusCode_CAPTURED ControlStatusCode = 280 //Traffic captured by plugin, ask Zoraxy not to process the traffic + ControlStatusCode_UNHANDLED ControlStatusCode = 284 //Traffic not handled by plugin, ask Zoraxy to process the traffic + ControlStatusCode_ERROR ControlStatusCode = 580 //Error occurred while processing the traffic, ask Zoraxy to process the traffic and log the error +) + +type SubscriptionEvent struct { + EventName string `json:"event_name"` + EventSource string `json:"event_source"` + Payload string `json:"payload"` //Payload of the event, can be empty +} + +type RuntimeConstantValue struct { + ZoraxyVersion string `json:"zoraxy_version"` + ZoraxyUUID string `json:"zoraxy_uuid"` + DevelopmentBuild bool `json:"development_build"` //Whether the Zoraxy is a development build or not +} + +type PermittedAPIEndpoint struct { + Method string `json:"method"` //HTTP method for the API endpoint (e.g., GET, POST) + Endpoint string `json:"endpoint"` //The API endpoint that the plugin can access + Reason string `json:"reason"` //The reason why the plugin needs to access this endpoint +} + +/* +IntroSpect Payload + +When the plugin is initialized with -introspect flag, +the plugin shell return this payload as JSON and exit +*/ +type IntroSpect struct { + /* Plugin metadata */ + ID string `json:"id"` //Unique ID of your plugin, recommended using your own domain in reverse like com.yourdomain.pluginname + Name string `json:"name"` //Name of your plugin + Author string `json:"author"` //Author name of your plugin + AuthorContact string `json:"author_contact"` //Author contact of your plugin, like email + Description string `json:"description"` //Description of your plugin + URL string `json:"url"` //URL of your plugin + Type PluginType `json:"type"` //Type of your plugin, Router(0) or Utilities(1) + VersionMajor int `json:"version_major"` //Major version of your plugin + VersionMinor int `json:"version_minor"` //Minor version of your plugin + VersionPatch int `json:"version_patch"` //Patch version of your plugin + + /* + + Endpoint Settings + + */ + + /* + Static Capture Settings + + Once plugin is enabled these rules always applies to the enabled HTTP Proxy rule + This is faster than dynamic capture, but less flexible + */ + StaticCapturePaths []StaticCaptureRule `json:"static_capture_paths"` //Static capture paths of your plugin, see Zoraxy documentation for more details + StaticCaptureIngress string `json:"static_capture_ingress"` //Static capture ingress path of your plugin (e.g. /s_handler) + + /* + Dynamic Capture Settings + + Once plugin is enabled, these rules will be captured and forward to plugin sniff + if the plugin sniff returns 280, the traffic will be captured + otherwise, the traffic will be forwarded to the next plugin + This is slower than static capture, but more flexible + */ + DynamicCaptureSniff string `json:"dynamic_capture_sniff"` //Dynamic capture sniff path of your plugin (e.g. /d_sniff) + DynamicCaptureIngress string `json:"dynamic_capture_ingress"` //Dynamic capture ingress path of your plugin (e.g. /d_handler) + + /* UI Path for your plugin */ + UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI + + /* Subscriptions Settings */ + SubscriptionPath string `json:"subscription_path"` //Subscription event path of your plugin (e.g. /notifyme), a POST request with SubscriptionEvent as body will be sent to this path when the event is triggered + SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, paired with comments describing how the event is used, see Zoraxy documentation for more details + + /* API Access Control */ + PermittedAPIEndpoints []PermittedAPIEndpoint `json:"permitted_api_endpoints"` //List of API endpoints this plugin can access, and a description of why the plugin needs to access this endpoint +} + +/* +ServeIntroSpect Function + +This function will check if the plugin is initialized with -introspect flag, +if so, it will print the intro spect and exit + +Place this function at the beginning of your plugin main function +*/ +func ServeIntroSpect(pluginSpect *IntroSpect) { + if len(os.Args) > 1 && os.Args[1] == "-introspect" { + //Print the intro spect and exit + jsonData, _ := json.MarshalIndent(pluginSpect, "", " ") + fmt.Println(string(jsonData)) + os.Exit(0) + } +} + +/* +ConfigureSpec Payload + +Zoraxy will start your plugin with -configure flag, +the plugin shell read this payload as JSON and configure itself +by the supplied values like starting a web server at given port +that listens to 127.0.0.1:port +*/ +type ConfigureSpec struct { + Port int `json:"port"` //Port to listen + RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values + APIKey string `json:"api_key,omitempty"` //API key for accessing Zoraxy APIs, if the plugin has permitted endpoints + ZoraxyPort int `json:"zoraxy_port,omitempty"` //The port that Zoraxy is running on, used for making API calls to Zoraxy + //To be expanded +} + +/* +RecvExecuteConfigureSpec Function + +This function will read the configure spec from Zoraxy +and return the ConfigureSpec object + +Place this function after ServeIntroSpect function in your plugin main function +*/ +func RecvConfigureSpec() (*ConfigureSpec, error) { + for i, arg := range os.Args { + if strings.HasPrefix(arg, "-configure=") { + var configSpec ConfigureSpec + if err := json.Unmarshal([]byte(arg[11:]), &configSpec); err != nil { + return nil, err + } + return &configSpec, nil + } else if arg == "-configure" { + var configSpec ConfigureSpec + var nextArg string + if len(os.Args) > i+1 { + nextArg = os.Args[i+1] + if err := json.Unmarshal([]byte(nextArg), &configSpec); err != nil { + return nil, err + } + } else { + return nil, fmt.Errorf("No port specified after -configure flag") + } + return &configSpec, nil + } + } + return nil, fmt.Errorf("No -configure flag found") +} + +/* +ServeAndRecvSpec Function + +This function will serve the intro spect and return the configure spec +See the ServeIntroSpect and RecvConfigureSpec for more details +*/ +func ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error) { + ServeIntroSpect(pluginSpect) + return RecvConfigureSpec() +} diff --git a/example/plugins/api-call-example/ui.go b/example/plugins/api-call-example/ui.go new file mode 100644 index 0000000..d8fccc4 --- /dev/null +++ b/example/plugins/api-call-example/ui.go @@ -0,0 +1,223 @@ +package main + +import ( + "fmt" + "html" + "net/http" + "net/http/httputil" + "strconv" + + plugin "aroz.org/zoraxy/api-call-example/mod/zoraxy_plugin" +) + +func allowedEndpoint(cfg *plugin.ConfigureSpec) (string, error) { + // Make an API call to the permitted endpoint + client := &http.Client{} + apiURL := fmt.Sprintf("http://localhost:%d/api/access/list", cfg.ZoraxyPort) + req, err := http.NewRequest(http.MethodGet, apiURL, nil) + if err != nil { + return "", fmt.Errorf("error creating request: %v", err) + } + // Make sure to set the Authorization header + req.Header.Set("Authorization", "Bearer "+cfg.APIKey) // Use the API key from the runtime config + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("error making API call: %v", err) + } + defer resp.Body.Close() + + // Check if the response status is OK + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("received non-OK response status %d", resp.StatusCode) + } + + respDump, err := httputil.DumpResponse(resp, true) + if err != nil { + + return "", fmt.Errorf("error dumping response: %v", err) + } + + return string(respDump), nil +} + +func allowedEndpointInvalidKey(cfg *plugin.ConfigureSpec) (string, error) { + // Make an API call to the permitted endpoint with an invalid key + client := &http.Client{} + apiURL := fmt.Sprintf("http://localhost:%d/api/access/list", cfg.ZoraxyPort) + req, err := http.NewRequest(http.MethodGet, apiURL, nil) + if err != nil { + return "", fmt.Errorf("error creating request: %v", err) + } + // Use an invalid API key + req.Header.Set("Authorization", "Bearer invalid-key") + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("error making API call: %v", err) + } + defer resp.Body.Close() + + respDump, err := httputil.DumpResponse(resp, true) + if err != nil { + + return "", fmt.Errorf("error dumping response: %v", err) + } + + return string(respDump), nil +} + +func disallowedEndpoint(cfg *plugin.ConfigureSpec) (string, error) { + // Make an API call to an endpoint that is not permitted + client := &http.Client{} + apiURL := fmt.Sprintf("http://localhost:%d/api/acme/listExpiredDomains", cfg.ZoraxyPort) + req, err := http.NewRequest(http.MethodGet, apiURL, nil) + if err != nil { + return "", fmt.Errorf("error creating request: %v", err) + } + // Use the API key from the runtime config + req.Header.Set("Authorization", "Bearer "+cfg.APIKey) + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("error making API call: %v", err) + } + defer resp.Body.Close() + + respDump, err := httputil.DumpResponse(resp, true) + if err != nil { + return "", fmt.Errorf("error dumping response: %v", err) + } + + return string(respDump), nil +} + +func RenderUI(config *plugin.ConfigureSpec, w http.ResponseWriter, r *http.Request) { + // make several types of API calls to demonstrate the plugin functionality + accessList, err := allowedEndpoint(config) + var RenderedAccessListHTML string + if err != nil { + RenderedAccessListHTML = fmt.Sprintf("

Error fetching access list: %v

", err) + } else { + // Render the access list as HTML + RenderedAccessListHTML = fmt.Sprintf("
%s
", html.EscapeString(accessList)) + } + + // Make an API call with an invalid key + invalidKeyResponse, err := allowedEndpointInvalidKey(config) + var RenderedInvalidKeyResponseHTML string + if err != nil { + RenderedInvalidKeyResponseHTML = fmt.Sprintf("

Error with invalid key: %v

", err) + } else { + // Render the invalid key response as HTML + RenderedInvalidKeyResponseHTML = fmt.Sprintf("
%s
", html.EscapeString(invalidKeyResponse)) + } + + // Make an API call to an endpoint that is not permitted + disallowedResponse, err := disallowedEndpoint(config) + var RenderedDisallowedResponseHTML string + if err != nil { + RenderedDisallowedResponseHTML = fmt.Sprintf("

Error with disallowed endpoint: %v

", err) + } else { + // Render the disallowed response as HTML + RenderedDisallowedResponseHTML = fmt.Sprintf("
%s
", html.EscapeString(disallowedResponse)) + } + + // Render the UI for the plugin + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(http.StatusOK) + html := ` + + + + Plugin UI + + + + + + + + + + +

Welcome to the Plugin UI

+

Plugin is running on port: ` + strconv.Itoa(config.Port) + `

+ +

API Call Examples

+ +
+

✅ Allowed Endpoint (Valid API Key)

+

Making a GET request to /api/access/list with a valid API key:

+
+ ` + RenderedAccessListHTML + ` +
+
+ +
+

❌ Invalid API Key

+

Making a GET request to /api/access/list with an invalid API key:

+
+ ` + RenderedInvalidKeyResponseHTML + ` +
+
+ +
+

⚠️ Disallowed Endpoint

+

Making a GET request to /api/acme/listExpiredDomains (not in allowed endpoints):

+
+ ` + RenderedDisallowedResponseHTML + ` +
+
+ + ` + w.Write([]byte(html)) +} From 2d43890fcfdbc13142b85605aa3e0594f87de164 Mon Sep 17 00:00:00 2001 From: Anthony Rubick <68485672+AnthonyMichaelTDM@users.noreply.github.com> Date: Thu, 17 Jul 2025 20:25:05 -0700 Subject: [PATCH 03/12] feat: Add .gitignore for Docker to exclude example and src directories --- docker/.gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 docker/.gitignore diff --git a/docker/.gitignore b/docker/.gitignore new file mode 100644 index 0000000..5e82536 --- /dev/null +++ b/docker/.gitignore @@ -0,0 +1,2 @@ +example/ +src/ \ No newline at end of file From 46cfc024932c257e534f2b0fd293f538938aeb46 Mon Sep 17 00:00:00 2001 From: Anthony Rubick <68485672+AnthonyMichaelTDM@users.noreply.github.com> Date: Thu, 17 Jul 2025 22:47:57 -0700 Subject: [PATCH 04/12] feat(webui/plugininfo): Add section for permitted API endpoints --- src/web/snippet/pluginInfo.html | 36 +++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/web/snippet/pluginInfo.html b/src/web/snippet/pluginInfo.html index 692a06b..71e514d 100644 --- a/src/web/snippet/pluginInfo.html +++ b/src/web/snippet/pluginInfo.html @@ -139,6 +139,26 @@
+

Plugin IntroSpect Permitted API Endpoints

+

The following API endpoints are registered by this plugin and will be accessible by the plugin's API key:

+ + + + + + + + + + + +
EndpointMethodReason
+

+ Note that the API endpoints are only accessible by the plugin's API key. + If the plugin does not have an API key, it will not be able to access these endpoints. + API keys are generated automatically by Zoraxy when a plugin with permitted API endpoints is enabled. +

+
@@ -219,6 +239,22 @@ $("#dynamic_capture_sniffing_path").text(dynamicCaptureSniffingPath); $("#dynamic_capture_ingress").text(dynamicCaptureIngress); $("#registered_ui_proxy_path").text(registeredUIProxyPath); + + //Update permitted API endpoints + let apiEndpoints = data.Spec.permitted_api_endpoints; + if (apiEndpoints == null || apiEndpoints.length == 0) { + $("#plugin_permitted_api_endpoints").html('No API endpoints registered'); + } else { + let endpointRows = ''; + apiEndpoints.forEach(function(endpoint) { + endpointRows += ` + ${endpoint.endpoint} + ${endpoint.method} + ${endpoint.reason} + `; + }); + $("#plugin_permitted_api_endpoints").html(endpointRows); + } }); } From ed8f9b733706a69d039c277d03bb9d10c5571b92 Mon Sep 17 00:00:00 2001 From: Anthony Rubick <68485672+AnthonyMichaelTDM@users.noreply.github.com> Date: Thu, 17 Jul 2025 23:18:40 -0700 Subject: [PATCH 05/12] fix(plugin-auth): check both endpoint and method --- src/mod/auth/plugin_apikey_manager.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mod/auth/plugin_apikey_manager.go b/src/mod/auth/plugin_apikey_manager.go index 7a06553..cef838a 100644 --- a/src/mod/auth/plugin_apikey_manager.go +++ b/src/mod/auth/plugin_apikey_manager.go @@ -84,7 +84,7 @@ func (m *APIKeyManager) ValidateAPIKeyForEndpoint(endpoint string, method string // Check if the endpoint is permitted for _, permittedEndpoint := range pluginAPIKey.PermittedEndpoints { - if permittedEndpoint.Endpoint == endpoint { + if permittedEndpoint.Endpoint == endpoint && permittedEndpoint.Method == method { return pluginAPIKey, nil } } From d187c32a8a5b42f99f6561f0ec86101f2ead3ef5 Mon Sep 17 00:00:00 2001 From: Anthony Rubick <68485672+AnthonyMichaelTDM@users.noreply.github.com> Date: Thu, 17 Jul 2025 23:19:38 -0700 Subject: [PATCH 06/12] feat(plugins): add an example api call to an accessible but unpermitted endpoint --- example/plugins/api-call-example/ui.go | 68 +++++++++++++++++++++----- 1 file changed, 56 insertions(+), 12 deletions(-) diff --git a/example/plugins/api-call-example/ui.go b/example/plugins/api-call-example/ui.go index d8fccc4..00dfecf 100644 --- a/example/plugins/api-call-example/ui.go +++ b/example/plugins/api-call-example/ui.go @@ -69,7 +69,7 @@ func allowedEndpointInvalidKey(cfg *plugin.ConfigureSpec) (string, error) { return string(respDump), nil } -func disallowedEndpoint(cfg *plugin.ConfigureSpec) (string, error) { +func unaccessibleEndpoint(cfg *plugin.ConfigureSpec) (string, error) { // Make an API call to an endpoint that is not permitted client := &http.Client{} apiURL := fmt.Sprintf("http://localhost:%d/api/acme/listExpiredDomains", cfg.ZoraxyPort) @@ -95,6 +95,32 @@ func disallowedEndpoint(cfg *plugin.ConfigureSpec) (string, error) { return string(respDump), nil } +func unpermittedEndpoint(cfg *plugin.ConfigureSpec) (string, error) { + // Make an API call to an endpoint that is plugin-accessible but is not permitted + client := &http.Client{} + apiURL := fmt.Sprintf("http://localhost:%d/api/proxy/list", cfg.ZoraxyPort) + req, err := http.NewRequest(http.MethodGet, apiURL, nil) + if err != nil { + return "", fmt.Errorf("error creating request: %v", err) + } + // Use the API key from the runtime config + req.Header.Set("Authorization", "Bearer "+cfg.APIKey) + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("error making API call: %v", err) + } + defer resp.Body.Close() + + respDump, err := httputil.DumpResponse(resp, true) + if err != nil { + return "", fmt.Errorf("error dumping response: %v", err) + } + + return string(respDump), nil +} + func RenderUI(config *plugin.ConfigureSpec, w http.ResponseWriter, r *http.Request) { // make several types of API calls to demonstrate the plugin functionality accessList, err := allowedEndpoint(config) @@ -116,14 +142,24 @@ func RenderUI(config *plugin.ConfigureSpec, w http.ResponseWriter, r *http.Reque RenderedInvalidKeyResponseHTML = fmt.Sprintf("
%s
", html.EscapeString(invalidKeyResponse)) } - // Make an API call to an endpoint that is not permitted - disallowedResponse, err := disallowedEndpoint(config) - var RenderedDisallowedResponseHTML string + // Make an API call to an endpoint that is not plugin-accessible + unaccessibleResponse, err := unaccessibleEndpoint(config) + var RenderedUnaccessibleResponseHTML string if err != nil { - RenderedDisallowedResponseHTML = fmt.Sprintf("

Error with disallowed endpoint: %v

", err) + RenderedUnaccessibleResponseHTML = fmt.Sprintf("

Error with unaccessible endpoint: %v

", err) } else { - // Render the disallowed response as HTML - RenderedDisallowedResponseHTML = fmt.Sprintf("
%s
", html.EscapeString(disallowedResponse)) + // Render the unaccessible response as HTML + RenderedUnaccessibleResponseHTML = fmt.Sprintf("
%s
", html.EscapeString(unaccessibleResponse)) + } + + // Make an API call to an endpoint that is plugin-accessible but is not permitted + unpermittedResponse, err := unpermittedEndpoint(config) + var RenderedUnpermittedResponseHTML string + if err != nil { + RenderedUnpermittedResponseHTML = fmt.Sprintf("

Error with unpermitted endpoint: %v

", err) + } else { + // Render the unpermitted response as HTML + RenderedUnpermittedResponseHTML = fmt.Sprintf("
%s
", html.EscapeString(unpermittedResponse)) } // Render the UI for the plugin @@ -133,7 +169,7 @@ func RenderUI(config *plugin.ConfigureSpec, w http.ResponseWriter, r *http.Reque - Plugin UI + API Call Example Plugin UI @@ -188,8 +224,8 @@ func RenderUI(config *plugin.ConfigureSpec, w http.ResponseWriter, r *http.Reque - -

Welcome to the Plugin UI

+ +

Welcome to the API Call Example Plugin UI

Plugin is running on port: ` + strconv.Itoa(config.Port) + `

API Call Examples

@@ -212,9 +248,17 @@ func RenderUI(config *plugin.ConfigureSpec, w http.ResponseWriter, r *http.Reque

⚠️ Disallowed Endpoint

-

Making a GET request to /api/acme/listExpiredDomains (not in allowed endpoints):

+

Making a GET request to /api/acme/listExpiredDomains (not a plugin-accessible endpoint):

- ` + RenderedDisallowedResponseHTML + ` + ` + RenderedUnaccessibleResponseHTML + ` +
+
+ +
+

⚠️ Unpermitted Endpoint

+

Making a GET request to /api/proxy/list (plugin-accessible but not permitted):

+
+ ` + RenderedUnpermittedResponseHTML + `
From 39b5da36d9387136a8aac52e8706c9eaa7d21b61 Mon Sep 17 00:00:00 2001 From: Anthony Rubick <68485672+AnthonyMichaelTDM@users.noreply.github.com> Date: Sat, 19 Jul 2025 21:37:25 -0700 Subject: [PATCH 07/12] refactor: partial revert of dd93f9a2c4f6a3476f178e9da83acb088ff477a5 --- src/api.go | 326 ++++++++++++++++++++--------------------- src/mod/auth/auth.go | 8 +- src/mod/auth/router.go | 43 +++--- 3 files changed, 187 insertions(+), 190 deletions(-) diff --git a/src/api.go b/src/api.go index b93c351..17fd3c4 100644 --- a/src/api.go +++ b/src/api.go @@ -25,233 +25,233 @@ import ( // Register the APIs for HTTP proxy management functions func RegisterHTTPProxyAPIs(authRouter *auth.RouterDef) { /* Reverse Proxy Settings & Status */ - authRouter.HandleFunc("/api/proxy/enable", ReverseProxyHandleOnOff, false) - authRouter.HandleFunc("/api/proxy/add", ReverseProxyHandleAddEndpoint, false) - authRouter.HandleFunc("/api/proxy/status", ReverseProxyStatus, true) - authRouter.HandleFunc("/api/proxy/toggle", ReverseProxyToggleRuleSet, false) - authRouter.HandleFunc("/api/proxy/list", ReverseProxyList, true) - authRouter.HandleFunc("/api/proxy/listTags", ReverseProxyListTags, true) - authRouter.HandleFunc("/api/proxy/detail", ReverseProxyListDetail, true) - authRouter.HandleFunc("/api/proxy/edit", ReverseProxyHandleEditEndpoint, false) - authRouter.HandleFunc("/api/proxy/setAlias", ReverseProxyHandleAlias, false) - authRouter.HandleFunc("/api/proxy/setTlsConfig", ReverseProxyHandleSetTlsConfig, false) - authRouter.HandleFunc("/api/proxy/setHostname", ReverseProxyHandleSetHostname, false) - authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint, false) - authRouter.HandleFunc("/api/proxy/updateCredentials", UpdateProxyBasicAuthCredentials, false) - authRouter.HandleFunc("/api/proxy/tlscheck", domainsniff.HandleCheckSiteSupportTLS, false) - authRouter.HandleFunc("/api/proxy/setIncoming", HandleIncomingPortSet, false) - authRouter.HandleFunc("/api/proxy/useHttpsRedirect", HandleUpdateHttpsRedirect, false) - authRouter.HandleFunc("/api/proxy/listenPort80", HandleUpdatePort80Listener, false) - authRouter.HandleFunc("/api/proxy/requestIsProxied", HandleManagementProxyCheck, false) - authRouter.HandleFunc("/api/proxy/developmentMode", HandleDevelopmentModeChange, false) + authRouter.HandleFunc("/api/proxy/enable", ReverseProxyHandleOnOff) + authRouter.HandleFunc("/api/proxy/add", ReverseProxyHandleAddEndpoint) + authRouter.HandleFunc("/api/proxy/status", ReverseProxyStatus) + authRouter.HandleFunc("/api/proxy/toggle", ReverseProxyToggleRuleSet) + authRouter.HandleFunc("/api/proxy/list", ReverseProxyList) + authRouter.HandleFunc("/api/proxy/listTags", ReverseProxyListTags) + authRouter.HandleFunc("/api/proxy/detail", ReverseProxyListDetail) + authRouter.HandleFunc("/api/proxy/edit", ReverseProxyHandleEditEndpoint) + authRouter.HandleFunc("/api/proxy/setAlias", ReverseProxyHandleAlias) + authRouter.HandleFunc("/api/proxy/setTlsConfig", ReverseProxyHandleSetTlsConfig) + authRouter.HandleFunc("/api/proxy/setHostname", ReverseProxyHandleSetHostname) + authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint) + authRouter.HandleFunc("/api/proxy/updateCredentials", UpdateProxyBasicAuthCredentials) + authRouter.HandleFunc("/api/proxy/tlscheck", domainsniff.HandleCheckSiteSupportTLS) + authRouter.HandleFunc("/api/proxy/setIncoming", HandleIncomingPortSet) + authRouter.HandleFunc("/api/proxy/useHttpsRedirect", HandleUpdateHttpsRedirect) + authRouter.HandleFunc("/api/proxy/listenPort80", HandleUpdatePort80Listener) + authRouter.HandleFunc("/api/proxy/requestIsProxied", HandleManagementProxyCheck) + authRouter.HandleFunc("/api/proxy/developmentMode", HandleDevelopmentModeChange) /* Reverse proxy upstream (load balance) */ - authRouter.HandleFunc("/api/proxy/upstream/list", ReverseProxyUpstreamList, false) - authRouter.HandleFunc("/api/proxy/upstream/add", ReverseProxyUpstreamAdd, false) - authRouter.HandleFunc("/api/proxy/upstream/setPriority", ReverseProxyUpstreamSetPriority, false) - authRouter.HandleFunc("/api/proxy/upstream/update", ReverseProxyUpstreamUpdate, false) - authRouter.HandleFunc("/api/proxy/upstream/remove", ReverseProxyUpstreamDelete, false) + authRouter.HandleFunc("/api/proxy/upstream/list", ReverseProxyUpstreamList) + authRouter.HandleFunc("/api/proxy/upstream/add", ReverseProxyUpstreamAdd) + authRouter.HandleFunc("/api/proxy/upstream/setPriority", ReverseProxyUpstreamSetPriority) + authRouter.HandleFunc("/api/proxy/upstream/update", ReverseProxyUpstreamUpdate) + authRouter.HandleFunc("/api/proxy/upstream/remove", ReverseProxyUpstreamDelete) /* Reverse proxy virtual directory */ - authRouter.HandleFunc("/api/proxy/vdir/list", ReverseProxyListVdir, false) - authRouter.HandleFunc("/api/proxy/vdir/add", ReverseProxyAddVdir, false) - authRouter.HandleFunc("/api/proxy/vdir/del", ReverseProxyDeleteVdir, false) - authRouter.HandleFunc("/api/proxy/vdir/edit", ReverseProxyEditVdir, false) + authRouter.HandleFunc("/api/proxy/vdir/list", ReverseProxyListVdir) + authRouter.HandleFunc("/api/proxy/vdir/add", ReverseProxyAddVdir) + authRouter.HandleFunc("/api/proxy/vdir/del", ReverseProxyDeleteVdir) + authRouter.HandleFunc("/api/proxy/vdir/edit", ReverseProxyEditVdir) /* Reverse proxy user-defined header */ - authRouter.HandleFunc("/api/proxy/header/list", HandleCustomHeaderList, true) - authRouter.HandleFunc("/api/proxy/header/add", HandleCustomHeaderAdd, false) - authRouter.HandleFunc("/api/proxy/header/remove", HandleCustomHeaderRemove, false) - authRouter.HandleFunc("/api/proxy/header/handleHSTS", HandleHSTSState, false) - authRouter.HandleFunc("/api/proxy/header/handleHopByHop", HandleHopByHop, false) - authRouter.HandleFunc("/api/proxy/header/handleHostOverwrite", HandleHostOverwrite, false) - authRouter.HandleFunc("/api/proxy/header/handlePermissionPolicy", HandlePermissionPolicy, false) - authRouter.HandleFunc("/api/proxy/header/handleWsHeaderBehavior", HandleWsHeaderBehavior, false) + authRouter.HandleFunc("/api/proxy/header/list", HandleCustomHeaderList) + authRouter.HandleFunc("/api/proxy/header/add", HandleCustomHeaderAdd) + authRouter.HandleFunc("/api/proxy/header/remove", HandleCustomHeaderRemove) + authRouter.HandleFunc("/api/proxy/header/handleHSTS", HandleHSTSState) + authRouter.HandleFunc("/api/proxy/header/handleHopByHop", HandleHopByHop) + authRouter.HandleFunc("/api/proxy/header/handleHostOverwrite", HandleHostOverwrite) + authRouter.HandleFunc("/api/proxy/header/handlePermissionPolicy", HandlePermissionPolicy) + authRouter.HandleFunc("/api/proxy/header/handleWsHeaderBehavior", HandleWsHeaderBehavior) /* Reverse proxy auth related */ - authRouter.HandleFunc("/api/proxy/auth/exceptions/list", ListProxyBasicAuthExceptionPaths, true) - authRouter.HandleFunc("/api/proxy/auth/exceptions/add", AddProxyBasicAuthExceptionPaths, false) - authRouter.HandleFunc("/api/proxy/auth/exceptions/delete", RemoveProxyBasicAuthExceptionPaths, false) + authRouter.HandleFunc("/api/proxy/auth/exceptions/list", ListProxyBasicAuthExceptionPaths) + authRouter.HandleFunc("/api/proxy/auth/exceptions/add", AddProxyBasicAuthExceptionPaths) + authRouter.HandleFunc("/api/proxy/auth/exceptions/delete", RemoveProxyBasicAuthExceptionPaths) } // Register the APIs for TLS / SSL certificate management functions func RegisterTLSAPIs(authRouter *auth.RouterDef) { //Global certificate settings - authRouter.HandleFunc("/api/cert/tls", handleToggleTLSProxy, false) - authRouter.HandleFunc("/api/cert/tlsRequireLatest", handleSetTlsRequireLatest, false) - authRouter.HandleFunc("/api/cert/resolve", handleCertTryResolve, false) - authRouter.HandleFunc("/api/cert/setPreferredCertificate", handleSetDomainPreferredCertificate, false) + authRouter.HandleFunc("/api/cert/tls", handleToggleTLSProxy) + authRouter.HandleFunc("/api/cert/tlsRequireLatest", handleSetTlsRequireLatest) + authRouter.HandleFunc("/api/cert/resolve", handleCertTryResolve) + authRouter.HandleFunc("/api/cert/setPreferredCertificate", handleSetDomainPreferredCertificate) //Certificate store functions - authRouter.HandleFunc("/api/cert/upload", tlsCertManager.HandleCertUpload, false) - authRouter.HandleFunc("/api/cert/download", tlsCertManager.HandleCertDownload, false) - authRouter.HandleFunc("/api/cert/list", tlsCertManager.HandleListCertificate, false) - authRouter.HandleFunc("/api/cert/listdomains", tlsCertManager.HandleListDomains, false) - authRouter.HandleFunc("/api/cert/checkDefault", tlsCertManager.HandleDefaultCertCheck, false) - authRouter.HandleFunc("/api/cert/delete", tlsCertManager.HandleCertRemove, false) - authRouter.HandleFunc("/api/cert/selfsign", tlsCertManager.HandleSelfSignCertGenerate, false) + authRouter.HandleFunc("/api/cert/upload", tlsCertManager.HandleCertUpload) + authRouter.HandleFunc("/api/cert/download", tlsCertManager.HandleCertDownload) + authRouter.HandleFunc("/api/cert/list", tlsCertManager.HandleListCertificate) + authRouter.HandleFunc("/api/cert/listdomains", tlsCertManager.HandleListDomains) + authRouter.HandleFunc("/api/cert/checkDefault", tlsCertManager.HandleDefaultCertCheck) + authRouter.HandleFunc("/api/cert/delete", tlsCertManager.HandleCertRemove) + authRouter.HandleFunc("/api/cert/selfsign", tlsCertManager.HandleSelfSignCertGenerate) } // Register the APIs for Authentication handlers like Forward Auth and OAUTH2 func RegisterAuthenticationHandlerAPIs(authRouter *auth.RouterDef) { - authRouter.HandleFunc("/api/sso/forward-auth", forwardAuthRouter.HandleAPIOptions, false) - authRouter.HandleFunc("/api/sso/OAuth2", oauth2Router.HandleSetOAuth2Settings, false) + authRouter.HandleFunc("/api/sso/forward-auth", forwardAuthRouter.HandleAPIOptions) + authRouter.HandleFunc("/api/sso/OAuth2", oauth2Router.HandleSetOAuth2Settings) } // Register the APIs for redirection rules management functions func RegisterRedirectionAPIs(authRouter *auth.RouterDef) { - authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules, true) - authRouter.HandleFunc("/api/redirect/add", handleAddRedirectionRule, false) - authRouter.HandleFunc("/api/redirect/delete", handleDeleteRedirectionRule, false) - authRouter.HandleFunc("/api/redirect/edit", handleEditRedirectionRule, false) - authRouter.HandleFunc("/api/redirect/regex", handleToggleRedirectRegexpSupport, false) + authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules) + authRouter.HandleFunc("/api/redirect/add", handleAddRedirectionRule) + authRouter.HandleFunc("/api/redirect/delete", handleDeleteRedirectionRule) + authRouter.HandleFunc("/api/redirect/edit", handleEditRedirectionRule) + authRouter.HandleFunc("/api/redirect/regex", handleToggleRedirectRegexpSupport) } // Register the APIs for access rules management functions func RegisterAccessRuleAPIs(authRouter *auth.RouterDef) { /* Access Rules Settings & Status */ - authRouter.HandleFunc("/api/access/list", handleListAccessRules, true) - authRouter.HandleFunc("/api/access/attach", handleAttachRuleToHost, false) - authRouter.HandleFunc("/api/access/create", handleCreateAccessRule, false) - authRouter.HandleFunc("/api/access/remove", handleRemoveAccessRule, false) - authRouter.HandleFunc("/api/access/update", handleUpadateAccessRule, false) + authRouter.HandleFunc("/api/access/list", handleListAccessRules) + authRouter.HandleFunc("/api/access/attach", handleAttachRuleToHost) + authRouter.HandleFunc("/api/access/create", handleCreateAccessRule) + authRouter.HandleFunc("/api/access/remove", handleRemoveAccessRule) + authRouter.HandleFunc("/api/access/update", handleUpadateAccessRule) /* Blacklist */ - authRouter.HandleFunc("/api/blacklist/list", handleListBlacklisted, true) - authRouter.HandleFunc("/api/blacklist/country/add", handleCountryBlacklistAdd, false) - authRouter.HandleFunc("/api/blacklist/country/remove", handleCountryBlacklistRemove, false) - authRouter.HandleFunc("/api/blacklist/ip/add", handleIpBlacklistAdd, true) - authRouter.HandleFunc("/api/blacklist/ip/remove", handleIpBlacklistRemove, true) - authRouter.HandleFunc("/api/blacklist/enable", handleBlacklistEnable, true) + authRouter.HandleFunc("/api/blacklist/list", handleListBlacklisted) + authRouter.HandleFunc("/api/blacklist/country/add", handleCountryBlacklistAdd) + authRouter.HandleFunc("/api/blacklist/country/remove", handleCountryBlacklistRemove) + authRouter.HandleFunc("/api/blacklist/ip/add", handleIpBlacklistAdd) + authRouter.HandleFunc("/api/blacklist/ip/remove", handleIpBlacklistRemove) + authRouter.HandleFunc("/api/blacklist/enable", handleBlacklistEnable) /* Whitelist */ - authRouter.HandleFunc("/api/whitelist/list", handleListWhitelisted, true) - authRouter.HandleFunc("/api/whitelist/country/add", handleCountryWhitelistAdd, false) - authRouter.HandleFunc("/api/whitelist/country/remove", handleCountryWhitelistRemove, false) - authRouter.HandleFunc("/api/whitelist/ip/add", handleIpWhitelistAdd, false) - authRouter.HandleFunc("/api/whitelist/ip/remove", handleIpWhitelistRemove, false) - authRouter.HandleFunc("/api/whitelist/enable", handleWhitelistEnable, false) - authRouter.HandleFunc("/api/whitelist/allowLocal", handleWhitelistAllowLoopback, false) + authRouter.HandleFunc("/api/whitelist/list", handleListWhitelisted) + authRouter.HandleFunc("/api/whitelist/country/add", handleCountryWhitelistAdd) + authRouter.HandleFunc("/api/whitelist/country/remove", handleCountryWhitelistRemove) + authRouter.HandleFunc("/api/whitelist/ip/add", handleIpWhitelistAdd) + authRouter.HandleFunc("/api/whitelist/ip/remove", handleIpWhitelistRemove) + authRouter.HandleFunc("/api/whitelist/enable", handleWhitelistEnable) + authRouter.HandleFunc("/api/whitelist/allowLocal", handleWhitelistAllowLoopback) /* Quick Ban List */ - authRouter.HandleFunc("/api/quickban/list", handleListQuickBan, false) + authRouter.HandleFunc("/api/quickban/list", handleListQuickBan) } // Register the APIs for path blocking rules management functions, WIP func RegisterPathRuleAPIs(authRouter *auth.RouterDef) { - authRouter.HandleFunc("/api/pathrule/add", pathRuleHandler.HandleAddBlockingPath, false) - authRouter.HandleFunc("/api/pathrule/list", pathRuleHandler.HandleListBlockingPath, false) - authRouter.HandleFunc("/api/pathrule/remove", pathRuleHandler.HandleRemoveBlockingPath, false) + authRouter.HandleFunc("/api/pathrule/add", pathRuleHandler.HandleAddBlockingPath) + authRouter.HandleFunc("/api/pathrule/list", pathRuleHandler.HandleListBlockingPath) + authRouter.HandleFunc("/api/pathrule/remove", pathRuleHandler.HandleRemoveBlockingPath) } // Register the APIs statistic anlysis and uptime monitoring functions func RegisterStatisticalAPIs(authRouter *auth.RouterDef) { /* Traffic Summary */ - authRouter.HandleFunc("/api/stats/summary", statisticCollector.HandleTodayStatLoad, false) - authRouter.HandleFunc("/api/stats/countries", HandleCountryDistrSummary, false) - authRouter.HandleFunc("/api/stats/netstat", netstatBuffers.HandleGetNetworkInterfaceStats, false) - authRouter.HandleFunc("/api/stats/netstatgraph", netstatBuffers.HandleGetBufferedNetworkInterfaceStats, false) - authRouter.HandleFunc("/api/stats/listnic", netstat.HandleListNetworkInterfaces, false) + authRouter.HandleFunc("/api/stats/summary", statisticCollector.HandleTodayStatLoad) + authRouter.HandleFunc("/api/stats/countries", HandleCountryDistrSummary) + authRouter.HandleFunc("/api/stats/netstat", netstatBuffers.HandleGetNetworkInterfaceStats) + authRouter.HandleFunc("/api/stats/netstatgraph", netstatBuffers.HandleGetBufferedNetworkInterfaceStats) + authRouter.HandleFunc("/api/stats/listnic", netstat.HandleListNetworkInterfaces) /* Zoraxy Analytic */ - authRouter.HandleFunc("/api/analytic/list", AnalyticLoader.HandleSummaryList, false) - authRouter.HandleFunc("/api/analytic/load", AnalyticLoader.HandleLoadTargetDaySummary, false) - authRouter.HandleFunc("/api/analytic/loadRange", AnalyticLoader.HandleLoadTargetRangeSummary, false) - authRouter.HandleFunc("/api/analytic/exportRange", AnalyticLoader.HandleRangeExport, false) - authRouter.HandleFunc("/api/analytic/resetRange", AnalyticLoader.HandleRangeReset, false) + authRouter.HandleFunc("/api/analytic/list", AnalyticLoader.HandleSummaryList) + authRouter.HandleFunc("/api/analytic/load", AnalyticLoader.HandleLoadTargetDaySummary) + authRouter.HandleFunc("/api/analytic/loadRange", AnalyticLoader.HandleLoadTargetRangeSummary) + authRouter.HandleFunc("/api/analytic/exportRange", AnalyticLoader.HandleRangeExport) + authRouter.HandleFunc("/api/analytic/resetRange", AnalyticLoader.HandleRangeReset) /* UpTime Monitor */ - authRouter.HandleFunc("/api/utm/list", HandleUptimeMonitorListing, false) + authRouter.HandleFunc("/api/utm/list", HandleUptimeMonitorListing) } // Register the APIs for Stream (TCP / UDP) Proxy management functions func RegisterStreamProxyAPIs(authRouter *auth.RouterDef) { - authRouter.HandleFunc("/api/streamprox/config/add", streamProxyManager.HandleAddProxyConfig, false) - authRouter.HandleFunc("/api/streamprox/config/edit", streamProxyManager.HandleEditProxyConfigs, false) - authRouter.HandleFunc("/api/streamprox/config/list", streamProxyManager.HandleListConfigs, false) - authRouter.HandleFunc("/api/streamprox/config/start", streamProxyManager.HandleStartProxy, false) - authRouter.HandleFunc("/api/streamprox/config/stop", streamProxyManager.HandleStopProxy, false) - authRouter.HandleFunc("/api/streamprox/config/delete", streamProxyManager.HandleRemoveProxy, false) - authRouter.HandleFunc("/api/streamprox/config/status", streamProxyManager.HandleGetProxyStatus, false) + authRouter.HandleFunc("/api/streamprox/config/add", streamProxyManager.HandleAddProxyConfig) + authRouter.HandleFunc("/api/streamprox/config/edit", streamProxyManager.HandleEditProxyConfigs) + authRouter.HandleFunc("/api/streamprox/config/list", streamProxyManager.HandleListConfigs) + authRouter.HandleFunc("/api/streamprox/config/start", streamProxyManager.HandleStartProxy) + authRouter.HandleFunc("/api/streamprox/config/stop", streamProxyManager.HandleStopProxy) + authRouter.HandleFunc("/api/streamprox/config/delete", streamProxyManager.HandleRemoveProxy) + authRouter.HandleFunc("/api/streamprox/config/status", streamProxyManager.HandleGetProxyStatus) } // Register the APIs for mDNS service management functions func RegisterMDNSAPIs(authRouter *auth.RouterDef) { - authRouter.HandleFunc("/api/mdns/list", HandleMdnsListing, false) - authRouter.HandleFunc("/api/mdns/discover", HandleMdnsScanning, false) + authRouter.HandleFunc("/api/mdns/list", HandleMdnsListing) + authRouter.HandleFunc("/api/mdns/discover", HandleMdnsScanning) } // Register the APIs for ACME and Auto Renewer management functions func RegisterACMEAndAutoRenewerAPIs(authRouter *auth.RouterDef) { /* ACME Core */ - authRouter.HandleFunc("/api/acme/listExpiredDomains", acmeHandler.HandleGetExpiredDomains, false) - authRouter.HandleFunc("/api/acme/obtainCert", AcmeCheckAndHandleRenewCertificate, false) + authRouter.HandleFunc("/api/acme/listExpiredDomains", acmeHandler.HandleGetExpiredDomains) + authRouter.HandleFunc("/api/acme/obtainCert", AcmeCheckAndHandleRenewCertificate) /* Auto Renewer */ - authRouter.HandleFunc("/api/acme/autoRenew/enable", acmeAutoRenewer.HandleAutoRenewEnable, false) - authRouter.HandleFunc("/api/acme/autoRenew/ca", HandleACMEPreferredCA, false) - authRouter.HandleFunc("/api/acme/autoRenew/email", acmeAutoRenewer.HandleACMEEmail, false) - authRouter.HandleFunc("/api/acme/autoRenew/setDomains", acmeAutoRenewer.HandleSetAutoRenewDomains, false) - authRouter.HandleFunc("/api/acme/autoRenew/setEAB", acmeAutoRenewer.HanldeSetEAB, false) - authRouter.HandleFunc("/api/acme/autoRenew/setDNS", acmeAutoRenewer.HandleSetDNS, false) - authRouter.HandleFunc("/api/acme/autoRenew/listDomains", acmeAutoRenewer.HandleLoadAutoRenewDomains, false) - authRouter.HandleFunc("/api/acme/autoRenew/renewPolicy", acmeAutoRenewer.HandleRenewPolicy, false) - authRouter.HandleFunc("/api/acme/autoRenew/renewNow", acmeAutoRenewer.HandleRenewNow, false) - authRouter.HandleFunc("/api/acme/dns/providers", acmedns.HandleServeProvidersJson, false) + authRouter.HandleFunc("/api/acme/autoRenew/enable", acmeAutoRenewer.HandleAutoRenewEnable) + authRouter.HandleFunc("/api/acme/autoRenew/ca", HandleACMEPreferredCA) + authRouter.HandleFunc("/api/acme/autoRenew/email", acmeAutoRenewer.HandleACMEEmail) + authRouter.HandleFunc("/api/acme/autoRenew/setDomains", acmeAutoRenewer.HandleSetAutoRenewDomains) + authRouter.HandleFunc("/api/acme/autoRenew/setEAB", acmeAutoRenewer.HanldeSetEAB) + authRouter.HandleFunc("/api/acme/autoRenew/setDNS", acmeAutoRenewer.HandleSetDNS) + authRouter.HandleFunc("/api/acme/autoRenew/listDomains", acmeAutoRenewer.HandleLoadAutoRenewDomains) + authRouter.HandleFunc("/api/acme/autoRenew/renewPolicy", acmeAutoRenewer.HandleRenewPolicy) + authRouter.HandleFunc("/api/acme/autoRenew/renewNow", acmeAutoRenewer.HandleRenewNow) + authRouter.HandleFunc("/api/acme/dns/providers", acmedns.HandleServeProvidersJson) /* ACME Wizard */ - authRouter.HandleFunc("/api/acme/wizard", acmewizard.HandleGuidedStepCheck, false) + authRouter.HandleFunc("/api/acme/wizard", acmewizard.HandleGuidedStepCheck) } // Register the APIs for Static Web Server management functions func RegisterStaticWebServerAPIs(authRouter *auth.RouterDef) { /* Static Web Server Controls */ - authRouter.HandleFunc("/api/webserv/status", staticWebServer.HandleGetStatus, false) - authRouter.HandleFunc("/api/webserv/start", staticWebServer.HandleStartServer, false) - authRouter.HandleFunc("/api/webserv/stop", staticWebServer.HandleStopServer, false) - authRouter.HandleFunc("/api/webserv/setPort", HandleStaticWebServerPortChange, false) - authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing, false) - authRouter.HandleFunc("/api/webserv/disableListenAllInterface", staticWebServer.SetDisableListenToAllInterface, false) + authRouter.HandleFunc("/api/webserv/status", staticWebServer.HandleGetStatus) + authRouter.HandleFunc("/api/webserv/start", staticWebServer.HandleStartServer) + authRouter.HandleFunc("/api/webserv/stop", staticWebServer.HandleStopServer) + authRouter.HandleFunc("/api/webserv/setPort", HandleStaticWebServerPortChange) + authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing) + authRouter.HandleFunc("/api/webserv/disableListenAllInterface", staticWebServer.SetDisableListenToAllInterface) /* File Manager */ if *allowWebFileManager { - authRouter.HandleFunc("/api/fs/list", staticWebServer.FileManager.HandleList, false) - authRouter.HandleFunc("/api/fs/upload", staticWebServer.FileManager.HandleUpload, false) - authRouter.HandleFunc("/api/fs/download", staticWebServer.FileManager.HandleDownload, false) - authRouter.HandleFunc("/api/fs/newFolder", staticWebServer.FileManager.HandleNewFolder, false) - authRouter.HandleFunc("/api/fs/copy", staticWebServer.FileManager.HandleFileCopy, false) - authRouter.HandleFunc("/api/fs/move", staticWebServer.FileManager.HandleFileMove, false) - authRouter.HandleFunc("/api/fs/properties", staticWebServer.FileManager.HandleFileProperties, false) - authRouter.HandleFunc("/api/fs/del", staticWebServer.FileManager.HandleFileDelete, false) + authRouter.HandleFunc("/api/fs/list", staticWebServer.FileManager.HandleList) + authRouter.HandleFunc("/api/fs/upload", staticWebServer.FileManager.HandleUpload) + authRouter.HandleFunc("/api/fs/download", staticWebServer.FileManager.HandleDownload) + authRouter.HandleFunc("/api/fs/newFolder", staticWebServer.FileManager.HandleNewFolder) + authRouter.HandleFunc("/api/fs/copy", staticWebServer.FileManager.HandleFileCopy) + authRouter.HandleFunc("/api/fs/move", staticWebServer.FileManager.HandleFileMove) + authRouter.HandleFunc("/api/fs/properties", staticWebServer.FileManager.HandleFileProperties) + authRouter.HandleFunc("/api/fs/del", staticWebServer.FileManager.HandleFileDelete) } } // Register the APIs for Network Utilities functions func RegisterNetworkUtilsAPIs(authRouter *auth.RouterDef) { - authRouter.HandleFunc("/api/tools/ipscan", ipscan.HandleIpScan, false) - authRouter.HandleFunc("/api/tools/portscan", ipscan.HandleScanPort, false) - authRouter.HandleFunc("/api/tools/traceroute", netutils.HandleTraceRoute, false) - authRouter.HandleFunc("/api/tools/ping", netutils.HandlePing, false) - authRouter.HandleFunc("/api/tools/whois", netutils.HandleWhois, false) - authRouter.HandleFunc("/api/tools/webssh", HandleCreateProxySession, false) - authRouter.HandleFunc("/api/tools/websshSupported", HandleWebSshSupportCheck, false) - authRouter.HandleFunc("/api/tools/wol", HandleWakeOnLan, false) - authRouter.HandleFunc("/api/tools/smtp/get", HandleSMTPGet, false) - authRouter.HandleFunc("/api/tools/smtp/set", HandleSMTPSet, false) - authRouter.HandleFunc("/api/tools/smtp/admin", HandleAdminEmailGet, false) - authRouter.HandleFunc("/api/tools/smtp/test", HandleTestEmailSend, false) - authRouter.HandleFunc("/api/tools/fwdproxy/enable", forwardProxy.HandleToogle, false) - authRouter.HandleFunc("/api/tools/fwdproxy/port", forwardProxy.HandlePort, false) + authRouter.HandleFunc("/api/tools/ipscan", ipscan.HandleIpScan) + authRouter.HandleFunc("/api/tools/portscan", ipscan.HandleScanPort) + authRouter.HandleFunc("/api/tools/traceroute", netutils.HandleTraceRoute) + authRouter.HandleFunc("/api/tools/ping", netutils.HandlePing) + authRouter.HandleFunc("/api/tools/whois", netutils.HandleWhois) + authRouter.HandleFunc("/api/tools/webssh", HandleCreateProxySession) + authRouter.HandleFunc("/api/tools/websshSupported", HandleWebSshSupportCheck) + authRouter.HandleFunc("/api/tools/wol", HandleWakeOnLan) + authRouter.HandleFunc("/api/tools/smtp/get", HandleSMTPGet) + authRouter.HandleFunc("/api/tools/smtp/set", HandleSMTPSet) + authRouter.HandleFunc("/api/tools/smtp/admin", HandleAdminEmailGet) + authRouter.HandleFunc("/api/tools/smtp/test", HandleTestEmailSend) + authRouter.HandleFunc("/api/tools/fwdproxy/enable", forwardProxy.HandleToogle) + authRouter.HandleFunc("/api/tools/fwdproxy/port", forwardProxy.HandlePort) } func RegisterPluginAPIs(authRouter *auth.RouterDef) { - authRouter.HandleFunc("/api/plugins/list", pluginManager.HandleListPlugins, false) - authRouter.HandleFunc("/api/plugins/enable", pluginManager.HandleEnablePlugin, false) - authRouter.HandleFunc("/api/plugins/disable", pluginManager.HandleDisablePlugin, false) - authRouter.HandleFunc("/api/plugins/icon", pluginManager.HandleLoadPluginIcon, false) - authRouter.HandleFunc("/api/plugins/info", pluginManager.HandlePluginInfo, false) + authRouter.HandleFunc("/api/plugins/list", pluginManager.HandleListPlugins) + authRouter.HandleFunc("/api/plugins/enable", pluginManager.HandleEnablePlugin) + authRouter.HandleFunc("/api/plugins/disable", pluginManager.HandleDisablePlugin) + authRouter.HandleFunc("/api/plugins/icon", pluginManager.HandleLoadPluginIcon) + authRouter.HandleFunc("/api/plugins/info", pluginManager.HandlePluginInfo) - authRouter.HandleFunc("/api/plugins/groups/list", pluginManager.HandleListPluginGroups, false) - authRouter.HandleFunc("/api/plugins/groups/add", pluginManager.HandleAddPluginToGroup, false) - authRouter.HandleFunc("/api/plugins/groups/remove", pluginManager.HandleRemovePluginFromGroup, false) - authRouter.HandleFunc("/api/plugins/groups/deleteTag", pluginManager.HandleRemovePluginGroup, false) + authRouter.HandleFunc("/api/plugins/groups/list", pluginManager.HandleListPluginGroups) + authRouter.HandleFunc("/api/plugins/groups/add", pluginManager.HandleAddPluginToGroup) + authRouter.HandleFunc("/api/plugins/groups/remove", pluginManager.HandleRemovePluginFromGroup) + authRouter.HandleFunc("/api/plugins/groups/deleteTag", pluginManager.HandleRemovePluginGroup) - authRouter.HandleFunc("/api/plugins/store/list", pluginManager.HandleListDownloadablePlugins, false) - authRouter.HandleFunc("/api/plugins/store/resync", pluginManager.HandleResyncPluginList, false) - authRouter.HandleFunc("/api/plugins/store/install", pluginManager.HandleInstallPlugin, false) - authRouter.HandleFunc("/api/plugins/store/uninstall", pluginManager.HandleUninstallPlugin, false) + authRouter.HandleFunc("/api/plugins/store/list", pluginManager.HandleListDownloadablePlugins) + authRouter.HandleFunc("/api/plugins/store/resync", pluginManager.HandleResyncPluginList) + authRouter.HandleFunc("/api/plugins/store/install", pluginManager.HandleInstallPlugin) + authRouter.HandleFunc("/api/plugins/store/uninstall", pluginManager.HandleUninstallPlugin) // Developer options - authRouter.HandleFunc("/api/plugins/developer/enableAutoReload", pluginManager.HandleEnableHotReload, false) - authRouter.HandleFunc("/api/plugins/developer/setAutoReloadInterval", pluginManager.HandleSetHotReloadInterval, false) + authRouter.HandleFunc("/api/plugins/developer/enableAutoReload", pluginManager.HandleEnableHotReload) + authRouter.HandleFunc("/api/plugins/developer/setAutoReloadInterval", pluginManager.HandleSetHotReloadInterval) } // Register the APIs for Auth functions, due to scoping issue some functions are defined here @@ -372,17 +372,17 @@ func initAPIs(targetMux *http.ServeMux) { targetMux.HandleFunc("/api/account/new", HandleNewPasswordSetup) //Docker UX Optimizations - authRouter.HandleFunc("/api/docker/available", DockerUXOptimizer.HandleDockerAvailable, false) - authRouter.HandleFunc("/api/docker/containers", DockerUXOptimizer.HandleDockerContainersList, false) + authRouter.HandleFunc("/api/docker/available", DockerUXOptimizer.HandleDockerAvailable) + authRouter.HandleFunc("/api/docker/containers", DockerUXOptimizer.HandleDockerContainersList) //Others targetMux.HandleFunc("/api/info/x", HandleZoraxyInfo) - authRouter.HandleFunc("/api/info/geoip", HandleGeoIpLookup, false) - authRouter.HandleFunc("/api/conf/export", ExportConfigAsZip, false) - authRouter.HandleFunc("/api/conf/import", ImportConfigFromZip, false) - authRouter.HandleFunc("/api/log/list", LogViewer.HandleListLog, false) - authRouter.HandleFunc("/api/log/read", LogViewer.HandleReadLog, false) + authRouter.HandleFunc("/api/info/geoip", HandleGeoIpLookup) + authRouter.HandleFunc("/api/conf/export", ExportConfigAsZip) + authRouter.HandleFunc("/api/conf/import", ImportConfigFromZip) + authRouter.HandleFunc("/api/log/list", LogViewer.HandleListLog) + authRouter.HandleFunc("/api/log/read", LogViewer.HandleReadLog) //Debug - authRouter.HandleFunc("/api/info/pprof", pprof.Index, false) + authRouter.HandleFunc("/api/info/pprof", pprof.Index) } diff --git a/src/mod/auth/auth.go b/src/mod/auth/auth.go index d6ce56f..350a972 100644 --- a/src/mod/auth/auth.go +++ b/src/mod/auth/auth.go @@ -28,8 +28,6 @@ type AuthAgent struct { Database *db.Database LoginRedirectionHandler func(http.ResponseWriter, *http.Request) Logger *logger.Logger - //Plugin related - PluginAuthMiddleware *PluginAuthMiddleware //Plugin authentication middleware } type AuthEndpoints struct { @@ -41,7 +39,7 @@ type AuthEndpoints struct { } // Constructor -func NewAuthenticationAgent(sessionName string, key []byte, sysdb *db.Database, allowReg bool, systemLogger *logger.Logger, loginRedirectionHandler func(http.ResponseWriter, *http.Request), apiKeyManager *APIKeyManager) *AuthAgent { +func NewAuthenticationAgent(sessionName string, key []byte, sysdb *db.Database, allowReg bool, systemLogger *logger.Logger, loginRedirectionHandler func(http.ResponseWriter, *http.Request)) *AuthAgent { store := sessions.NewCookieStore(key) err := sysdb.NewTable("auth") if err != nil { @@ -49,9 +47,6 @@ func NewAuthenticationAgent(sessionName string, key []byte, sysdb *db.Database, panic(err) } - //Initialize the plugin authentication middleware - pluginAuthMiddleware := NewPluginAuthMiddleware(apiKeyManager) - //Create a new AuthAgent object newAuthAgent := AuthAgent{ SessionName: sessionName, @@ -59,7 +54,6 @@ func NewAuthenticationAgent(sessionName string, key []byte, sysdb *db.Database, Database: sysdb, LoginRedirectionHandler: loginRedirectionHandler, Logger: systemLogger, - PluginAuthMiddleware: pluginAuthMiddleware, } //Return the authAgent diff --git a/src/mod/auth/router.go b/src/mod/auth/router.go index 572df8e..85f9dae 100644 --- a/src/mod/auth/router.go +++ b/src/mod/auth/router.go @@ -25,7 +25,7 @@ func NewManagedHTTPRouter(option RouterOption) *RouterDef { } } -func (router *RouterDef) HandleFunc(endpoint string, handler func(http.ResponseWriter, *http.Request), pluginAccessible bool) error { +func (router *RouterDef) HandleFunc(endpoint string, handler func(http.ResponseWriter, *http.Request)) error { //Check if the endpoint already registered if _, exist := router.endpoints[endpoint]; exist { fmt.Println("WARNING! Duplicated registering of web endpoint: " + endpoint) @@ -34,28 +34,31 @@ func (router *RouterDef) HandleFunc(endpoint string, handler func(http.ResponseW authAgent := router.option.AuthAgent - authWrapper := func(w http.ResponseWriter, r *http.Request) { - //Check authentication of the user - X_Plugin_Auth := r.Header.Get("X-Zoraxy-Plugin-Auth") - if router.option.RequireAuth && !(pluginAccessible && X_Plugin_Auth == "true") { - authAgent.HandleCheckAuth(w, r, func(w http.ResponseWriter, r *http.Request) { - handler(w, r) - }) - } else { - handler(w, r) - } - } - - // if the endpoint is supposed to be plugin accessible, wrap it with plugin authentication middleware - if pluginAccessible { - authWrapper = router.option.AuthAgent.PluginAuthMiddleware.WrapHandler(endpoint, authWrapper) - } - //OK. Register handler if router.option.TargetMux == nil { - http.HandleFunc(endpoint, authWrapper) + http.HandleFunc(endpoint, func(w http.ResponseWriter, r *http.Request) { + //Check authentication of the user + if router.option.RequireAuth { + authAgent.HandleCheckAuth(w, r, func(w http.ResponseWriter, r *http.Request) { + handler(w, r) + }) + } else { + handler(w, r) + } + + }) } else { - router.option.TargetMux.HandleFunc(endpoint, authWrapper) + router.option.TargetMux.HandleFunc(endpoint, func(w http.ResponseWriter, r *http.Request) { + //Check authentication of the user + if router.option.RequireAuth { + authAgent.HandleCheckAuth(w, r, func(w http.ResponseWriter, r *http.Request) { + handler(w, r) + }) + } else { + handler(w, r) + } + + }) } router.endpoints[endpoint] = handler From f9e51bfd27ff581191ad5b8190a8c6251961ed3d Mon Sep 17 00:00:00 2001 From: Anthony Rubick <68485672+AnthonyMichaelTDM@users.noreply.github.com> Date: Sat, 19 Jul 2025 21:44:56 -0700 Subject: [PATCH 08/12] remove unused functions --- src/mod/auth/plugin_apikey_manager.go | 40 --------------------------- src/mod/auth/plugin_middleware.go | 17 +----------- 2 files changed, 1 insertion(+), 56 deletions(-) diff --git a/src/mod/auth/plugin_apikey_manager.go b/src/mod/auth/plugin_apikey_manager.go index cef838a..2803239 100644 --- a/src/mod/auth/plugin_apikey_manager.go +++ b/src/mod/auth/plugin_apikey_manager.go @@ -92,19 +92,6 @@ func (m *APIKeyManager) ValidateAPIKeyForEndpoint(endpoint string, method string return nil, fmt.Errorf("endpoint not permitted for this API key") } -// RevokeAPIKey revokes an API key -func (m *APIKeyManager) RevokeAPIKey(apiKey string) error { - m.mutex.Lock() - defer m.mutex.Unlock() - - if _, exists := m.keys[apiKey]; !exists { - return fmt.Errorf("API key not found") - } - - delete(m.keys, apiKey) - return nil -} - // RevokeAPIKeysForPlugin revokes all API keys for a specific plugin func (m *APIKeyManager) RevokeAPIKeysForPlugin(pluginID string) error { m.mutex.Lock() @@ -123,30 +110,3 @@ func (m *APIKeyManager) RevokeAPIKeysForPlugin(pluginID string) error { return nil } - -// GetAPIKeyForPlugin returns the API key for a plugin (if exists) -func (m *APIKeyManager) GetAPIKeyForPlugin(pluginID string) (*PluginAPIKey, error) { - m.mutex.RLock() - defer m.mutex.RUnlock() - - for _, pluginAPIKey := range m.keys { - if pluginAPIKey.PluginID == pluginID { - return pluginAPIKey, nil - } - } - - return nil, fmt.Errorf("no API key found for plugin") -} - -// ListAPIKeys returns all API keys (for debugging purposes) -func (m *APIKeyManager) ListAPIKeys() []*PluginAPIKey { - m.mutex.RLock() - defer m.mutex.RUnlock() - - keys := make([]*PluginAPIKey, 0, len(m.keys)) - for _, pluginAPIKey := range m.keys { - keys = append(keys, pluginAPIKey) - } - - return keys -} diff --git a/src/mod/auth/plugin_middleware.go b/src/mod/auth/plugin_middleware.go index 9be7cb8..507d42e 100644 --- a/src/mod/auth/plugin_middleware.go +++ b/src/mod/auth/plugin_middleware.go @@ -19,11 +19,6 @@ func NewPluginAuthMiddleware(apiKeyManager *APIKeyManager) *PluginAuthMiddleware } } -// ValidatePluginAPIRequest validates an API request with plugin API key for a specific endpoint -func (m *PluginAuthMiddleware) ValidatePluginAPIRequest(endpoint string, method string, apiKey string) (*PluginAPIKey, error) { - return m.apiKeyManager.ValidateAPIKeyForEndpoint(endpoint, method, apiKey) -} - // WrapHandler wraps an HTTP handler with plugin authentication middleware func (m *PluginAuthMiddleware) WrapHandler(endpoint string, handler http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -50,7 +45,7 @@ func (m *PluginAuthMiddleware) WrapHandler(endpoint string, handler http.Handler apiKey := strings.TrimPrefix(authHeader, "Bearer ") // Validate the API key for this endpoint - pluginAPIKey, err := m.ValidatePluginAPIRequest(endpoint, r.Method, apiKey) + pluginAPIKey, err := m.apiKeyManager.ValidateAPIKeyForEndpoint(endpoint, r.Method, apiKey) if err != nil { // Invalid API key or endpoint not permitted http.Error(w, "Unauthorized: Invalid API key or endpoint not permitted", http.StatusUnauthorized) @@ -65,13 +60,3 @@ func (m *PluginAuthMiddleware) WrapHandler(endpoint string, handler http.Handler handler(w, r) } } - -// GetPluginIDFromRequest extracts the plugin ID from the request if authenticated via plugin API key -func GetPluginIDFromRequest(r *http.Request) string { - return r.Header.Get("X-Plugin-ID") -} - -// IsPluginAuthenticated checks if the request is authenticated via plugin API key -func IsPluginAuthenticated(r *http.Request) bool { - return r.Header.Get("X-Plugin-Auth") == "true" -} From be5f631b9fe11ed3d584a1266e5f716c0eeb5541 Mon Sep 17 00:00:00 2001 From: Anthony Rubick <68485672+AnthonyMichaelTDM@users.noreply.github.com> Date: Sat, 19 Jul 2025 22:29:02 -0700 Subject: [PATCH 09/12] refactor: reuse PluginAuthMiddleware as AuthAgent for plugin accessible endpoints --- src/mod/auth/plugin_middleware.go | 116 +++++++++++++++++++----------- src/start.go | 6 +- 2 files changed, 78 insertions(+), 44 deletions(-) diff --git a/src/mod/auth/plugin_middleware.go b/src/mod/auth/plugin_middleware.go index 507d42e..568c801 100644 --- a/src/mod/auth/plugin_middleware.go +++ b/src/mod/auth/plugin_middleware.go @@ -3,60 +3,92 @@ package auth import ( + "errors" + "fmt" "net/http" "strings" ) +const ( + PLUGIN_API_PREFIX = "/plugin" +) + +type PluginMiddlewareOptions struct { + DeniedHandler http.HandlerFunc //Thing(s) to do when request is rejected + ApiKeyManager *APIKeyManager + TargetMux *http.ServeMux +} + // PluginAuthMiddleware provides authentication middleware for plugin API requests type PluginAuthMiddleware struct { - apiKeyManager *APIKeyManager + option PluginMiddlewareOptions + endpoints map[string]http.HandlerFunc } // NewPluginAuthMiddleware creates a new plugin authentication middleware -func NewPluginAuthMiddleware(apiKeyManager *APIKeyManager) *PluginAuthMiddleware { +func NewPluginAuthMiddleware(option PluginMiddlewareOptions) *PluginAuthMiddleware { return &PluginAuthMiddleware{ - apiKeyManager: apiKeyManager, + option: option, + endpoints: make(map[string]http.HandlerFunc), } } -// WrapHandler wraps an HTTP handler with plugin authentication middleware -func (m *PluginAuthMiddleware) WrapHandler(endpoint string, handler http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - // First, remove any existing plugin authentication headers - r.Header.Del("X-Zoraxy-Plugin-ID") - r.Header.Del("X-Zoraxy-Plugin-Auth") - - // Check for API key in the Authorization header - authHeader := r.Header.Get("Authorization") - if authHeader == "" { - // No authorization header, proceed with normal authentication - handler(w, r) - return - } - - // Check if it's a plugin API key (Bearer token) - if !strings.HasPrefix(authHeader, "Bearer ") { - // Not a Bearer token, proceed with normal authentication - handler(w, r) - return - } - - // Extract the API key - apiKey := strings.TrimPrefix(authHeader, "Bearer ") - - // Validate the API key for this endpoint - pluginAPIKey, err := m.apiKeyManager.ValidateAPIKeyForEndpoint(endpoint, r.Method, apiKey) - if err != nil { - // Invalid API key or endpoint not permitted - http.Error(w, "Unauthorized: Invalid API key or endpoint not permitted", http.StatusUnauthorized) - return - } - - // Add plugin information to the request context - r.Header.Set("X-Zoraxy-Plugin-ID", pluginAPIKey.PluginID) - r.Header.Set("X-Zoraxy-Plugin-Auth", "true") - - // Call the original handler - handler(w, r) +func (m *PluginAuthMiddleware) HandleAuthCheck(w http.ResponseWriter, r *http.Request, handler http.HandlerFunc) { + // Check for API key in the Authorization header + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + // No authorization header + m.option.DeniedHandler(w, r) + return } + + // Check if it's a plugin API key (Bearer token) + if !strings.HasPrefix(authHeader, "Bearer ") { + // Not a Bearer token + m.option.DeniedHandler(w, r) + return + } + + // Extract the API key + apiKey := strings.TrimPrefix(authHeader, "Bearer ") + + // Validate the API key for this endpoint + _, err := m.option.ApiKeyManager.ValidateAPIKeyForEndpoint(r.URL.Path, r.Method, apiKey) + if err != nil { + // Invalid API key or endpoint not permitted + m.option.DeniedHandler(w, r) + return + } + + // Call the original handler + handler(w, r) +} + +// wraps an HTTP handler with plugin authentication middleware +func (m *PluginAuthMiddleware) HandleFunc(endpoint string, handler http.HandlerFunc) error { + // ensure the endpoint is prefixed with PLUGIN_API_PREFIX + if !strings.HasPrefix(endpoint, PLUGIN_API_PREFIX) { + endpoint = PLUGIN_API_PREFIX + endpoint + } + + // Check if the endpoint already registered + if _, exist := m.endpoints[endpoint]; exist { + fmt.Println("WARNING! Duplicated registering of plugin api endpoint: " + endpoint) + return errors.New("endpoint register duplicated") + } + + m.endpoints[endpoint] = handler + + wrappedHandler := func(w http.ResponseWriter, r *http.Request) { + m.HandleAuthCheck(w, r, handler) + } + + // Ok. Register handler + if m.option.TargetMux == nil { + http.HandleFunc(endpoint, wrappedHandler) + } else { + m.option.TargetMux.HandleFunc(endpoint, wrappedHandler) + } + + return nil } diff --git a/src/start.go b/src/start.go index 1cc55bf..b280871 100644 --- a/src/start.go +++ b/src/start.go @@ -91,7 +91,6 @@ func startupSequence() { os.MkdirAll(CONF_HTTP_PROXY, 0775) //Create an auth agent - pluginApiKeyManager = auth.NewAPIKeyManager() sessionKey, err := auth.GetSessionKey(sysdb, SystemWideLogger) if err != nil { log.Fatal(err) @@ -99,7 +98,10 @@ func startupSequence() { authAgent = auth.NewAuthenticationAgent(SYSTEM_NAME, []byte(sessionKey), sysdb, true, SystemWideLogger, func(w http.ResponseWriter, r *http.Request) { //Not logged in. Redirecting to login page http.Redirect(w, r, "/login.html", http.StatusTemporaryRedirect) - }, pluginApiKeyManager) + }) + + // Create an API key manager for plugin authentication + pluginApiKeyManager = auth.NewAPIKeyManager() //Create a TLS certificate manager tlsCertManager, err = tlscert.NewManager(CONF_CERT_STORE, SystemWideLogger) From e3e31d9f226619c2b11ac2282227fe66eb92f163 Mon Sep 17 00:00:00 2001 From: Anthony Rubick <68485672+AnthonyMichaelTDM@users.noreply.github.com> Date: Sat, 19 Jul 2025 22:29:22 -0700 Subject: [PATCH 10/12] feat: add the plugin accessible endpoints --- src/main.go | 1 + src/plugin_api.go | 135 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 src/plugin_api.go diff --git a/src/main.go b/src/main.go index f577d50..219a2a2 100644 --- a/src/main.go +++ b/src/main.go @@ -115,6 +115,7 @@ func main() { //Initiate management interface APIs requireAuth = !(*noauth) initAPIs(webminPanelMux) + initRestAPI(webminPanelMux) //Start the reverse proxy server in go routine go func() { diff --git a/src/plugin_api.go b/src/plugin_api.go new file mode 100644 index 0000000..21550b5 --- /dev/null +++ b/src/plugin_api.go @@ -0,0 +1,135 @@ +package main + +import ( + "net/http" + + "imuslab.com/zoraxy/mod/auth" + "imuslab.com/zoraxy/mod/netstat" +) + +// Register the APIs for HTTP proxy management functions +func RegisterHTTPProxyRestAPI(authMiddleware *auth.PluginAuthMiddleware) { + /* Reverse Proxy Settings & Status */ + authMiddleware.HandleFunc("/api/proxy/status", ReverseProxyStatus) + authMiddleware.HandleFunc("/api/proxy/list", ReverseProxyList) + authMiddleware.HandleFunc("/api/proxy/listTags", ReverseProxyListTags) + authMiddleware.HandleFunc("/api/proxy/detail", ReverseProxyListDetail) + /* Reverse proxy upstream (load balance) */ + authMiddleware.HandleFunc("/api/proxy/upstream/list", ReverseProxyUpstreamList) + /* Reverse proxy virtual directory */ + authMiddleware.HandleFunc("/api/proxy/vdir/list", ReverseProxyListVdir) + /* Reverse proxy user-defined header */ + authMiddleware.HandleFunc("/api/proxy/header/list", HandleCustomHeaderList) + /* Reverse proxy auth related */ + authMiddleware.HandleFunc("/api/proxy/auth/exceptions/list", ListProxyBasicAuthExceptionPaths) +} + +// Register the APIs for redirection rules management functions +func RegisterRedirectionRestAPI(authRouter *auth.PluginAuthMiddleware) { + authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules) +} + +// Register the APIs for access rules management functions +func RegisterAccessRuleRestAPI(authRouter *auth.PluginAuthMiddleware) { + /* Access Rules Settings & Status */ + authRouter.HandleFunc("/api/access/list", handleListAccessRules) + // authRouter.HandleFunc("/api/access/attach", handleAttachRuleToHost) + // authRouter.HandleFunc("/api/access/create", handleCreateAccessRule) + // authRouter.HandleFunc("/api/access/remove", handleRemoveAccessRule) + // authRouter.HandleFunc("/api/access/update", handleUpadateAccessRule) + /* Blacklist */ + authRouter.HandleFunc("/api/blacklist/list", handleListBlacklisted) + authRouter.HandleFunc("/api/blacklist/country/add", handleCountryBlacklistAdd) + authRouter.HandleFunc("/api/blacklist/country/remove", handleCountryBlacklistRemove) + authRouter.HandleFunc("/api/blacklist/ip/add", handleIpBlacklistAdd) + authRouter.HandleFunc("/api/blacklist/ip/remove", handleIpBlacklistRemove) + authRouter.HandleFunc("/api/blacklist/enable", handleBlacklistEnable) + /* Whitelist */ + authRouter.HandleFunc("/api/whitelist/list", handleListWhitelisted) + authRouter.HandleFunc("/api/whitelist/country/add", handleCountryWhitelistAdd) + authRouter.HandleFunc("/api/whitelist/country/remove", handleCountryWhitelistRemove) + authRouter.HandleFunc("/api/whitelist/ip/add", handleIpWhitelistAdd) + authRouter.HandleFunc("/api/whitelist/ip/remove", handleIpWhitelistRemove) + authRouter.HandleFunc("/api/whitelist/enable", handleWhitelistEnable) + authRouter.HandleFunc("/api/whitelist/allowLocal", handleWhitelistAllowLoopback) + /* Quick Ban List */ + authRouter.HandleFunc("/api/quickban/list", handleListQuickBan) +} + +// Register the APIs for path blocking rules management functions, WIP +func RegisterPathRuleRestAPI(authRouter *auth.PluginAuthMiddleware) { + authRouter.HandleFunc("/api/pathrule/list", pathRuleHandler.HandleListBlockingPath) +} + +// Register the APIs statistic anlysis and uptime monitoring functions +func RegisterStatisticalRestAPI(authRouter *auth.PluginAuthMiddleware) { + /* Traffic Summary */ + authRouter.HandleFunc("/api/stats/summary", statisticCollector.HandleTodayStatLoad) + authRouter.HandleFunc("/api/stats/countries", HandleCountryDistrSummary) + authRouter.HandleFunc("/api/stats/netstat", netstatBuffers.HandleGetNetworkInterfaceStats) + authRouter.HandleFunc("/api/stats/netstatgraph", netstatBuffers.HandleGetBufferedNetworkInterfaceStats) + authRouter.HandleFunc("/api/stats/listnic", netstat.HandleListNetworkInterfaces) + /* Zoraxy Analytic */ + authRouter.HandleFunc("/api/analytic/list", AnalyticLoader.HandleSummaryList) + authRouter.HandleFunc("/api/analytic/load", AnalyticLoader.HandleLoadTargetDaySummary) + authRouter.HandleFunc("/api/analytic/loadRange", AnalyticLoader.HandleLoadTargetRangeSummary) + authRouter.HandleFunc("/api/analytic/exportRange", AnalyticLoader.HandleRangeExport) + authRouter.HandleFunc("/api/analytic/resetRange", AnalyticLoader.HandleRangeReset) + /* UpTime Monitor */ + authRouter.HandleFunc("/api/utm/list", HandleUptimeMonitorListing) +} + +// Register the APIs for Stream (TCP / UDP) Proxy management functions +func RegisterStreamProxyRestAPI(authRouter *auth.PluginAuthMiddleware) { + authRouter.HandleFunc("/api/streamprox/config/list", streamProxyManager.HandleListConfigs) + authRouter.HandleFunc("/api/streamprox/config/status", streamProxyManager.HandleGetProxyStatus) +} + +// Register the APIs for mDNS service management functions +func RegisterMDNSRestAPI(authRouter *auth.PluginAuthMiddleware) { + authRouter.HandleFunc("/api/mdns/list", HandleMdnsListing) +} + +// Register the APIs for Static Web Server management functions +func RegisterStaticWebServerRestAPI(authRouter *auth.PluginAuthMiddleware) { + /* Static Web Server Controls */ + authRouter.HandleFunc("/api/webserv/status", staticWebServer.HandleGetStatus) + + /* File Manager */ + if *allowWebFileManager { + authRouter.HandleFunc("/api/fs/list", staticWebServer.FileManager.HandleList) + } +} + +func RegisterPluginRestAPI(authRouter *auth.PluginAuthMiddleware) { + authRouter.HandleFunc("/api/plugins/list", pluginManager.HandleListPlugins) + authRouter.HandleFunc("/api/plugins/info", pluginManager.HandlePluginInfo) + + authRouter.HandleFunc("/api/plugins/groups/list", pluginManager.HandleListPluginGroups) + + authRouter.HandleFunc("/api/plugins/store/list", pluginManager.HandleListDownloadablePlugins) +} + +/* Register all the APIs */ +func initRestAPI(targetMux *http.ServeMux) { + authMiddleware := auth.NewPluginAuthMiddleware( + auth.PluginMiddlewareOptions{ + TargetMux: targetMux, + ApiKeyManager: pluginApiKeyManager, + DeniedHandler: func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "401 - Unauthorized", http.StatusUnauthorized) + }, + }, + ) + + //Register the APIs + RegisterHTTPProxyRestAPI(authMiddleware) + RegisterRedirectionRestAPI(authMiddleware) + RegisterAccessRuleRestAPI(authMiddleware) + RegisterPathRuleRestAPI(authMiddleware) + RegisterStatisticalRestAPI(authMiddleware) + RegisterStreamProxyRestAPI(authMiddleware) + RegisterMDNSRestAPI(authMiddleware) + RegisterStaticWebServerRestAPI(authMiddleware) + RegisterPluginRestAPI(authMiddleware) +} From 40f915f7fbf563c02d4688930701bd7547c9f3a5 Mon Sep 17 00:00:00 2001 From: Anthony Rubick <68485672+AnthonyMichaelTDM@users.noreply.github.com> Date: Sat, 19 Jul 2025 22:35:44 -0700 Subject: [PATCH 11/12] fix: update example plugin --- example/plugins/api-call-example/ui.go | 31 +++++++++++++------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/example/plugins/api-call-example/ui.go b/example/plugins/api-call-example/ui.go index 00dfecf..b7ec67e 100644 --- a/example/plugins/api-call-example/ui.go +++ b/example/plugins/api-call-example/ui.go @@ -13,7 +13,7 @@ import ( func allowedEndpoint(cfg *plugin.ConfigureSpec) (string, error) { // Make an API call to the permitted endpoint client := &http.Client{} - apiURL := fmt.Sprintf("http://localhost:%d/api/access/list", cfg.ZoraxyPort) + apiURL := fmt.Sprintf("http://localhost:%d/plugin/api/access/list", cfg.ZoraxyPort) req, err := http.NewRequest(http.MethodGet, apiURL, nil) if err != nil { return "", fmt.Errorf("error creating request: %v", err) @@ -45,7 +45,7 @@ func allowedEndpoint(cfg *plugin.ConfigureSpec) (string, error) { func allowedEndpointInvalidKey(cfg *plugin.ConfigureSpec) (string, error) { // Make an API call to the permitted endpoint with an invalid key client := &http.Client{} - apiURL := fmt.Sprintf("http://localhost:%d/api/access/list", cfg.ZoraxyPort) + apiURL := fmt.Sprintf("http://localhost:%d/plugin/api/access/list", cfg.ZoraxyPort) req, err := http.NewRequest(http.MethodGet, apiURL, nil) if err != nil { return "", fmt.Errorf("error creating request: %v", err) @@ -98,7 +98,7 @@ func unaccessibleEndpoint(cfg *plugin.ConfigureSpec) (string, error) { func unpermittedEndpoint(cfg *plugin.ConfigureSpec) (string, error) { // Make an API call to an endpoint that is plugin-accessible but is not permitted client := &http.Client{} - apiURL := fmt.Sprintf("http://localhost:%d/api/proxy/list", cfg.ZoraxyPort) + apiURL := fmt.Sprintf("http://localhost:%d/plugin/api/proxy/list", cfg.ZoraxyPort) req, err := http.NewRequest(http.MethodGet, apiURL, nil) if err != nil { return "", fmt.Errorf("error creating request: %v", err) @@ -232,35 +232,36 @@ func RenderUI(config *plugin.ConfigureSpec, w http.ResponseWriter, r *http.Reque

✅ Allowed Endpoint (Valid API Key)

-

Making a GET request to /api/access/list with a valid API key:

+

Making a GET request to /plugin/api/access/list with a valid API key:

` + RenderedAccessListHTML + `
-
-

❌ Invalid API Key

-

Making a GET request to /api/access/list with an invalid API key:

+
+

⚠️ Invalid API Key

+

Making a GET request to /plugin/api/access/list with an invalid API key:

` + RenderedInvalidKeyResponseHTML + `
-

⚠️ Disallowed Endpoint

+

⚠️ Unpermitted Endpoint

+

Making a GET request to /plugin/api/proxy/list (not a permitted endpoint):

+
+ ` + RenderedUnpermittedResponseHTML + ` +
+
+ +
+

❌ Disallowed Endpoint

Making a GET request to /api/acme/listExpiredDomains (not a plugin-accessible endpoint):

` + RenderedUnaccessibleResponseHTML + `
-
-

⚠️ Unpermitted Endpoint

-

Making a GET request to /api/proxy/list (plugin-accessible but not permitted):

-
- ` + RenderedUnpermittedResponseHTML + ` -
-
` w.Write([]byte(html)) From ad2519d894280a02be3ff0f5f9aaf73c79e381d3 Mon Sep 17 00:00:00 2001 From: Anthony Rubick <68485672+AnthonyMichaelTDM@users.noreply.github.com> Date: Sat, 19 Jul 2025 22:50:42 -0700 Subject: [PATCH 12/12] fix(example plugin): update PermittedAPIEndpoints --- example/plugins/api-call-example/main.go | 2 +- example/plugins/api-call-example/ui.go | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/example/plugins/api-call-example/main.go b/example/plugins/api-call-example/main.go index bf78cac..fa2e751 100644 --- a/example/plugins/api-call-example/main.go +++ b/example/plugins/api-call-example/main.go @@ -32,7 +32,7 @@ func main() { PermittedAPIEndpoints: []plugin.PermittedAPIEndpoint{ { Method: http.MethodGet, - Endpoint: "/api/access/list", + Endpoint: "/plugin/api/access/list", Reason: "Used to display all configured Access Rules", }, }, diff --git a/example/plugins/api-call-example/ui.go b/example/plugins/api-call-example/ui.go index b7ec67e..68d49a3 100644 --- a/example/plugins/api-call-example/ui.go +++ b/example/plugins/api-call-example/ui.go @@ -28,17 +28,17 @@ func allowedEndpoint(cfg *plugin.ConfigureSpec) (string, error) { } defer resp.Body.Close() - // Check if the response status is OK - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("received non-OK response status %d", resp.StatusCode) - } - respDump, err := httputil.DumpResponse(resp, true) if err != nil { return "", fmt.Errorf("error dumping response: %v", err) } + // Check if the response status is OK + if resp.StatusCode != http.StatusOK { + return string(respDump), fmt.Errorf("received non-OK response status %d", resp.StatusCode) + } + return string(respDump), nil } @@ -126,7 +126,11 @@ func RenderUI(config *plugin.ConfigureSpec, w http.ResponseWriter, r *http.Reque accessList, err := allowedEndpoint(config) var RenderedAccessListHTML string if err != nil { - RenderedAccessListHTML = fmt.Sprintf("

Error fetching access list: %v

", err) + if accessList != "" { + RenderedAccessListHTML = fmt.Sprintf("

Error fetching access list: %v

%s
", err, html.EscapeString(accessList)) + } else { + RenderedAccessListHTML = fmt.Sprintf("

Error fetching access list: %v

", err) + } } else { // Render the access list as HTML RenderedAccessListHTML = fmt.Sprintf("
%s
", html.EscapeString(accessList))