mirror of
https://github.com/aykhans/gopkg-proxy.git
synced 2026-01-13 18:51:21 +00:00
first commit
This commit is contained in:
28
.github/workflows/docker.yml
vendored
Normal file
28
.github/workflows/docker.yml
vendored
Normal file
@@ -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
|
||||||
13
Dockerfile
Normal file
13
Dockerfile
Normal file
@@ -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"]
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -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.
|
||||||
38
README.md
Normal file
38
README.md
Normal file
@@ -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"
|
||||||
|
```
|
||||||
315
main.go
Normal file
315
main.go
Normal file
@@ -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 = `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="go-import" content="{{.Domain}}{{.Path}} {{.VCS}} {{.Repo}}">
|
||||||
|
<meta name="go-source" content="{{.Domain}}{{.Path}} {{.Repo}} {{.Repo}}/tree/master{/dir} {{.Repo}}/blob/master{/dir}/{file}#L{line}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
go get {{.Domain}}{{.Path}}
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
|
||||||
|
const redirectTemplate = `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="refresh" content="0; url=https://pkg.go.dev/{{.Domain}}{{.Path}}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
Redirecting to <a href="https://pkg.go.dev/{{.Domain}}{{.Path}}">pkg.go.dev/{{.Domain}}{{.Path}}</a>...
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
|
||||||
|
const homeTemplate = `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Go Packages - {{.Domain}}</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 50px auto;
|
||||||
|
padding: 20px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #00ADD8;
|
||||||
|
border-bottom: 2px solid #00ADD8;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
.package {
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-left: 4px solid #00ADD8;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 15px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.package-name {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 1.1em;
|
||||||
|
color: #00ADD8;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.package-repo {
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
.package-repo a {
|
||||||
|
color: #666;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.package-repo a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.install-cmd-wrapper {
|
||||||
|
position: relative;
|
||||||
|
margin-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.install-cmd {
|
||||||
|
background: #2d2d2d;
|
||||||
|
color: #f8f8f2;
|
||||||
|
padding: 10px;
|
||||||
|
padding-right: 45px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: monospace;
|
||||||
|
overflow-x: auto;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.copy-btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
background: #444;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #f8f8f2;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
.copy-btn:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
.copy-btn:active {
|
||||||
|
background: #00ADD8;
|
||||||
|
}
|
||||||
|
.copy-btn.copied {
|
||||||
|
background: #00ADD8;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
margin-top: 40px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Go Packages</h1>
|
||||||
|
|
||||||
|
{{range $index, $pkg := .Packages}}
|
||||||
|
<div class="package">
|
||||||
|
<div class="package-name">{{$.Domain}}{{$pkg.Path}}</div>
|
||||||
|
<div class="package-repo">
|
||||||
|
Source: <a href="{{$pkg.Repo}}" target="_blank">{{$pkg.Repo}}</a>
|
||||||
|
</div>
|
||||||
|
<div class="install-cmd-wrapper">
|
||||||
|
<div class="install-cmd" id="cmd-{{$index}}">go get {{$.Domain}}{{$pkg.Path}}</div>
|
||||||
|
<button class="copy-btn" onclick="copyToClipboard('cmd-{{$index}}', this)">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M13.5 5.5v7a1.5 1.5 0 0 1-1.5 1.5H5a1.5 1.5 0 0 1-1.5-1.5v-7A1.5 1.5 0 0 1 5 4h7a1.5 1.5 0 0 1 1.5 1.5z" stroke="currentColor" stroke-width="1.5" fill="none"/>
|
||||||
|
<path d="M5 4V3.5A1.5 1.5 0 0 1 6.5 2h7A1.5 1.5 0 0 1 15 3.5v7a1.5 1.5 0 0 1-1.5 1.5H13" stroke="currentColor" stroke-width="1.5" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>Total packages: {{.Count}}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function copyToClipboard(elementId, button) {
|
||||||
|
const element = document.getElementById(elementId);
|
||||||
|
const text = element.textContent;
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
// Visual feedback
|
||||||
|
button.classList.add('copied');
|
||||||
|
const originalHTML = button.innerHTML;
|
||||||
|
button.innerHTML = '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M13 4L6 11L3 8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
button.classList.remove('copied');
|
||||||
|
button.innerHTML = originalHTML;
|
||||||
|
}, 2000);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Failed to copy:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
Reference in New Issue
Block a user