commit ef2dd72d0d921f8b9bb64ffcdea3425df0de29de Author: Aykhan Shahsuvarov Date: Tue Dec 30 00:36:45 2025 +0400 first commit diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..2a5f1ce --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,28 @@ +name: Build and Push Docker Image + +on: + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set short SHA + run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: | + aykhans/gopkg-proxy:${{ env.SHORT_SHA }} + aykhans/gopkg-proxy:latest diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..35eec50 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM golang:1.25.5-alpine AS builder + +WORKDIR /app + +RUN --mount=type=bind,target=. CGO_ENABLED=0 go build -ldflags="-s -w" -o /server . + +FROM scratch + +COPY --from=builder /server /server + +EXPOSE 8421 + +ENTRYPOINT ["/server"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..89ae0f2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Aykhan Shahsuvarov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b75ab81 --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# gopkg-proxy + +A lightweight vanity import path server for Go packages. Use your own domain for clean, branded package imports. + +## Configuration + +### Add Packages + +Edit `main.go`: + +```go +var packages = []Package{ + { + Path: "/my-package", + Repo: "https://github.com/username/my-package", + VCS: "git", + }, +} +``` + +### Environment Variables + +- `PORT` - Server port (default: `8421`) +- `HOST_HEADER` - Header to read domain from (default: `X-Forwarded-Host`) + +## Usage + +Instead of: + +```go +import "github.com/aykhans/go-utils" +``` + +Use: + +```go +import "gopkg.yourdomain.com/go-utils" +``` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..98a85d6 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module gopkg-proxy + +go 1.25.5 diff --git a/main.go b/main.go new file mode 100644 index 0000000..a2c07ce --- /dev/null +++ b/main.go @@ -0,0 +1,315 @@ +package main + +import ( + "html/template" + "log" + "net/http" + "os" +) + +// Package represents a Go package mapping +type Package struct { + Path string // URL path like "/go-utils" + Repo string + VCS string // "git", "hg", etc. +} + +// Package mappings +var packages = []Package{ + { + Path: "/go-utils", + Repo: "https://github.com/aykhans/go-utils", + VCS: "git", + }, + { + Path: "/sarin", + Repo: "https://github.com/aykhans/sarin", + VCS: "git", + }, +} + +type HomeData struct { + Domain string + Packages []Package + Count int +} + +type VanityData struct { + Domain string + Path string + Repo string + VCS string +} + +type RedirectData struct { + Domain string + Path string +} + +var hostHeader = getEnvOrDefault("HOST_HEADER", "X-Forwarded-Host") + +func getEnvOrDefault(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} + +// getDomain extracts the domain from the request +func getDomain(r *http.Request) string { + if host := r.Header.Get(hostHeader); host != "" { + return host + } + return r.Host +} + +func homeHandler(w http.ResponseWriter, r *http.Request) { + data := HomeData{ + Domain: getDomain(r), + Packages: packages, + Count: len(packages), + } + + tmpl, err := template.New("home").Parse(homeTemplate) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if err := tmpl.Execute(w, data); err != nil { + log.Printf("Template execution error: %v", err) + } +} + +func packageHandler(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + + // Find package by path + var pkg *Package + for i := range packages { + if packages[i].Path == path { + pkg = &packages[i] + break + } + } + + if pkg == nil { + http.NotFound(w, r) + return + } + + // Check if this is a go-get request + if r.URL.Query().Get("go-get") == "1" { + // Respond with meta tags for go get + data := VanityData{ + Domain: getDomain(r), + Path: pkg.Path, + Repo: pkg.Repo, + VCS: pkg.VCS, + } + + tmpl, err := template.New("vanity").Parse(vanityTemplate) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if err := tmpl.Execute(w, data); err != nil { + log.Printf("Template execution error: %v", err) + } + } else { + // Regular browser request - redirect to pkg.go.dev + data := RedirectData{ + Domain: getDomain(r), + Path: pkg.Path, + } + + tmpl, err := template.New("redirect").Parse(redirectTemplate) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if err := tmpl.Execute(w, data); err != nil { + log.Printf("Template execution error: %v", err) + } + } +} + +func main() { + // Home page + http.HandleFunc("/{$}", homeHandler) + + // All package paths + http.HandleFunc("/", packageHandler) + + port := getEnvOrDefault("PORT", "8421") + log.Printf("Server listening on :%s", port) + log.Fatal(http.ListenAndServe(":"+port, nil)) +} + +const vanityTemplate = ` + + + + + + + go get {{.Domain}}{{.Path}} + +` + +const redirectTemplate = ` + + + + + + Redirecting to pkg.go.dev/{{.Domain}}{{.Path}}... + +` + +const homeTemplate = ` + + + + Go Packages - {{.Domain}} + + + +

Go Packages

+ + {{range $index, $pkg := .Packages}} +
+
{{$.Domain}}{{$pkg.Path}}
+
+ Source: {{$pkg.Repo}} +
+
+
go get {{$.Domain}}{{$pkg.Path}}
+ +
+
+ {{end}} + + + + + +`