diff --git a/.gitignore b/.gitignore index 5e9c044..8cdd224 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,9 @@ src/tmp/localhost.pem src/www/html/index.html src/sys.uuid src/zoraxy -src/log/ \ No newline at end of file +src/log/ + + +# dev-tags +/Dockerfile +/Entrypoint.sh \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index 2099358..5c3c5cd 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,19 +1,27 @@ +# Stage 1: Build Zoraxy FROM docker.io/golang:alpine AS build-zoraxy +# Install dependencies RUN mkdir -p /opt/zoraxy/source/ &&\ mkdir -p /usr/local/bin/ -# If you build it yourself, you will need to add the src directory into the docker directory. -COPY ./src/ /opt/zoraxy/source/ - WORKDIR /opt/zoraxy/source/ -RUN go mod tidy &&\ - go build -o /usr/local/bin/zoraxy &&\ +# Copy go.mod and go.sum first to leverage Docker cache +COPY ./src/go.mod ./src/go.sum ./ +RUN go mod tidy + +# Copy the rest of the source code +COPY ./src/ . + +# Build the application +RUN go build -o /usr/local/bin/zoraxy &&\ chmod 755 /usr/local/bin/zoraxy +# Stage 2: Build ZeroTier FROM docker.io/ubuntu:latest AS build-zerotier +# Install dependencies RUN mkdir -p /opt/zerotier/source/ &&\ mkdir -p /usr/local/bin/ @@ -22,6 +30,7 @@ WORKDIR /opt/zerotier/source/ RUN apt-get update -y &&\ apt-get install -y curl jq build-essential pkg-config clang cargo libssl-dev +# Download and build ZeroTier RUN curl -Lo ZeroTierOne.tar.gz https://codeload.github.com/zerotier/ZeroTierOne/tar.gz/refs/tags/1.10.6 &&\ tar -xzvf ZeroTierOne.tar.gz &&\ cd ZeroTierOne-* &&\ @@ -29,19 +38,24 @@ RUN curl -Lo ZeroTierOne.tar.gz https://codeload.github.com/zerotier/ZeroTierOne mv ./zerotier-one /usr/local/bin/zerotier-one &&\ chmod 755 /usr/local/bin/zerotier-one +# Stage 3: Final image FROM docker.io/ubuntu:latest +# Install runtime dependencies RUN apt-get update -y &&\ apt-get install -y bash sudo netcat-openbsd libssl-dev ca-certificates +# Copy entrypoint script COPY --chmod=700 ./entrypoint.sh /opt/zoraxy/ + +# Copy built binaries from previous stages COPY --from=build-zoraxy /usr/local/bin/zoraxy /usr/local/bin/zoraxy COPY --from=build-zerotier /usr/local/bin/zerotier-one /usr/local/bin/zerotier-one WORKDIR /opt/zoraxy/config/ +# Set environment variables ENV ZEROTIER="false" - ENV AUTORENEW="86400" ENV CFGUPGRADE="true" ENV DB="auto" @@ -60,9 +74,12 @@ ENV WEBROOT="./www" ENV ZTAUTH="" ENV ZTPORT="9993" +# Define volumes VOLUME [ "/opt/zoraxy/config/" ] +# Set entrypoint ENTRYPOINT [ "/opt/zoraxy/entrypoint.sh" ] +# Healthcheck HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=3 CMD nc -vz 127.0.0.1 $PORT || exit 1 diff --git a/src/config.go b/src/config.go index eacea02..62468f8 100644 --- a/src/config.go +++ b/src/config.go @@ -54,6 +54,11 @@ func LoadReverseProxyConfig(configFilepath string) error { return err } + //Make sure the tags are not nil + if thisConfigEndpoint.Tags == nil { + thisConfigEndpoint.Tags = []string{} + } + //Matching domain not set. Assume root if thisConfigEndpoint.RootOrMatchingDomain == "" { thisConfigEndpoint.RootOrMatchingDomain = "/" @@ -175,8 +180,8 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) { // Set the Content-Type header to indicate it's a zip file w.Header().Set("Content-Type", "application/zip") - // Set the Content-Disposition header to specify the file name - w.Header().Set("Content-Disposition", "attachment; filename=\"config.zip\"") + // Set the Content-Disposition header to specify the file name, add timestamp to the filename + w.Header().Set("Content-Disposition", "attachment; filename=\"zoraxy-config-"+time.Now().Format("2006-01-02-15-04-05")+".zip\"") // Create a zip writer zipWriter := zip.NewWriter(w) diff --git a/src/mod/dynamicproxy/typedef.go b/src/mod/dynamicproxy/typedef.go index c7a060d..2919a7e 100644 --- a/src/mod/dynamicproxy/typedef.go +++ b/src/mod/dynamicproxy/typedef.go @@ -194,6 +194,7 @@ type ProxyEndpoint struct { //Internal Logic Elements parent *Router `json:"-"` + Tags []string // Tags for the proxy endpoint } /* diff --git a/src/reverseproxy.go b/src/reverseproxy.go index 3ecdcb2..2f73e07 100644 --- a/src/reverseproxy.go +++ b/src/reverseproxy.go @@ -305,6 +305,15 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) { } } + tagStr, _ := utils.PostPara(r, "tags") + tags := []string{} + if tagStr != "" { + tags = strings.Split(tagStr, ",") + for i := range tags { + tags[i] = strings.TrimSpace(tags[i]) + } + } + var proxyEndpointCreated *dynamicproxy.ProxyEndpoint if eptype == "host" { rootOrMatchingDomain, err := utils.PostPara(r, "rootname") @@ -375,6 +384,8 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) { // Rate Limit RequireRateLimit: requireRateLimit, RateLimit: int64(proxyRateLimit), + + Tags: tags, } preparedEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisProxyEndpoint) @@ -533,6 +544,15 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) { return } + tagStr, _ := utils.PostPara(r, "tags") + tags := []string{} + if tagStr != "" { + tags = strings.Split(tagStr, ",") + for i := range tags { + tags[i] = strings.TrimSpace(tags[i]) + } + } + //Generate a new proxyEndpoint from the new config newProxyEndpoint := dynamicproxy.CopyEndpoint(targetProxyEntry) newProxyEndpoint.BypassGlobalTLS = bypassGlobalTLS @@ -557,6 +577,7 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) { newProxyEndpoint.RateLimit = proxyRateLimit newProxyEndpoint.UseStickySession = useStickySession newProxyEndpoint.DisableUptimeMonitor = disbleUtm + newProxyEndpoint.Tags = tags //Prepare to replace the current routing rule readyRoutingRule, err := dynamicProxyRouter.PrepareProxyRoute(newProxyEndpoint) diff --git a/src/web/components/httprp.html b/src/web/components/httprp.html index 8ecd402..1624fc5 100644 --- a/src/web/components/httprp.html +++ b/src/web/components/httprp.html @@ -12,6 +12,19 @@ min-width: 200px; } +
+ + +
+
@@ -19,6 +32,7 @@ + @@ -124,6 +138,9 @@ + `); }); + populateTagFilterDropdown(data); } resolveAccessRuleNameOnHostRPlist(); }); } + // Function to populate the tag filter dropdown + function populateTagFilterDropdown(data) { + let tags = new Set(); + data.forEach(subd => { + subd.Tags.forEach(tag => tags.add(tag)); + }); + tags = Array.from(tags).sort((a, b) => a.localeCompare(b)); + let dropdownMenu = $("#tagFilterDropdown .menu"); + dropdownMenu.html('
All
'); + tags.forEach(tag => { + dropdownMenu.append(`
${tag}
`); + }); + $('#tagFilterDropdown').dropdown(); + } + + // Function to filter the proxy list + function filterProxyList() { + let searchInput = $("#searchInput").val().toLowerCase(); + let selectedTag = $("#tagFilterDropdown").dropdown('get value'); + $("#httpProxyList tr").each(function() { + let host = $(this).find("td[data-label='']").text().toLowerCase(); + let tags = $(this).find("td[data-label='tags']").text().toLowerCase(); + if ((host.includes(searchInput) || searchInput === "") && (tags.includes(selectedTag) || selectedTag === "")) { + $(this).show(); + } else { + $(this).hide(); + } + }); + } + //Perform realtime alias update without refreshing the whole page function updateAliasListForEndpoint(endpointName, newAliasDomainList){ let targetEle = $(`.aliasDomains[eptuuid='${endpointName}']`); @@ -415,6 +463,7 @@ + `); $(".hostAccessRuleSelector").dropdown(); @@ -457,6 +506,7 @@ let requireRateLimit = $(row).find(".RequireRateLimit")[0].checked; let rateLimit = $(row).find(".RateLimit").val(); let bypassGlobalTLS = $(row).find(".BypassGlobalTLS")[0].checked; + let tags = $("#proxyTags").val().trim(); $.cjax({ url: "/api/proxy/edit", @@ -470,6 +520,7 @@ "authprovider" :authProviderType, "rate" :requireRateLimit, "ratenum" :rateLimit, + "tags": tags, }, success: function(data){ if (data.error !== undefined){ @@ -609,4 +660,24 @@ tabSwitchEventBind["httprp"] = function(){ listProxyEndpoints(); } + + function editTags(uuid){ + let payload = encodeURIComponent(JSON.stringify({ + ept: "host", + ep: uuid + })); + showSideWrapper("snippet/tagEditor.html?t=" + Date.now() + "#" + payload); + } + + // Initialize the proxy list on page load + $(document).ready(function() { + listProxyEndpoints(); + + // Event listener for clicking on tags + $(document).on('click', '.tag-select', function() { + let tag = $(this).text().trim(); + $('#tagFilterDropdown').dropdown('set selected', tag); + filterProxyList(); + }); + }); \ No newline at end of file diff --git a/src/web/components/rules.html b/src/web/components/rules.html index f6df6ce..642798f 100644 --- a/src/web/components/rules.html +++ b/src/web/components/rules.html @@ -63,6 +63,11 @@ +
+ + + Comma-separated list of tags for this proxy host. +
Security @@ -198,6 +203,7 @@ let skipWebSocketOriginCheck = $("#skipWebsocketOriginCheck")[0].checked; let accessRuleToUse = $("#newProxyRuleAccessFilter").val(); let useStickySessionLB = $("#useStickySessionLB")[0].checked; + let tags = $("#proxyTags").val().trim(); if (rootname.trim() == ""){ $("#rootname").parent().addClass("error"); @@ -231,6 +237,7 @@ cred: JSON.stringify(credentials), access: accessRuleToUse, stickysess: useStickySessionLB, + tags: tags, }, success: function(data){ if (data.error != undefined){ @@ -239,6 +246,7 @@ //Clear old data $("#rootname").val(""); $("#proxyDomain").val(""); + $("#proxyTags").val(""); credentials = []; updateTable(); reloadUptimeList(); diff --git a/src/web/snippet/tagEditor.html b/src/web/snippet/tagEditor.html new file mode 100644 index 0000000..e4c5a32 --- /dev/null +++ b/src/web/snippet/tagEditor.html @@ -0,0 +1,77 @@ + + + + + + + + + + + +
+
+
+ Edit Tags +
+
+
+
+

Enter tags for this proxy host. Use commas to separate multiple tags.

+
+
+ + +
+ + +
+
+ + + \ No newline at end of file
Host Destination Virtual DirectoryTags Advanced Settings Actions
${vdList} + ${subd.Tags.map(tag => `${tag}`).join("")} + ${subd.AuthenticationProvider.AuthMethod == 0x1?` Basic Auth`:``} ${subd.AuthenticationProvider.AuthMethod == 0x2?` Authelia`:``} @@ -142,12 +159,43 @@