Files
gh-downloader/app/main.go
Björn Benouarets 3a955c4238 Initial implementation of GitHub download proxy service
- Add flexible URL template configuration via environment variables
- Implement automatic repository path parsing and extraction
- Add 404 response handling for all non-200 status codes
- Support both SecNex and GitHub URL formats through URL template
- Include comprehensive documentation and Docker support
- Add proper Go project structure with .gitignore

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-14 17:21:50 +01:00

151 lines
3.6 KiB
Go

package main
import (
"fmt"
"io"
"net/http"
"os"
"path"
"strings"
"git.secnex.io/secnex/masterlog"
)
type Config struct {
Port string
Url string
}
func main() {
config := loadConfig()
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
proxyHandler(w, r, config)
})
masterlog.Info("Starting server", map[string]interface{}{
"port": config.Port,
"url": config.Url,
})
if err := http.ListenAndServe(config.Port, nil); err != nil {
masterlog.Error("Error starting server", map[string]interface{}{"error": err})
os.Exit(1)
}
}
func loadConfig() *Config {
config := &Config{
Url: getEnv("URL", "https://git.secnex.io/secnex/%s/raw/branch/main/%s"),
Port: getEnv("PORT", ":8080"),
}
return config
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
func proxyHandler(w http.ResponseWriter, r *http.Request, config *Config) {
// Only handle GET requests
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Extract repository and file path from URL
// Expected format: /repositoryName/path/to/file
cleanPath := path.Clean(r.URL.Path)
if cleanPath == "/" || cleanPath == "" {
http.Error(w, "Repository name required", http.StatusBadRequest)
return
}
pathParts := strings.Split(strings.TrimPrefix(cleanPath, "/"), "/")
if len(pathParts) < 1 {
http.Error(w, "Invalid path format", http.StatusBadRequest)
return
}
repository := pathParts[0]
filePath := strings.Join(pathParts[1:], "/")
// Log the incoming request
masterlog.Info("Incoming request", map[string]interface{}{
"method": r.Method,
"repository": repository,
"file_path": filePath,
"original_path": r.URL.Path,
})
// Build target URL based on server type
var targetURL string
targetURL = fmt.Sprintf(config.Url, repository, filePath)
// Create HTTP client with timeout
client := &http.Client{}
req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, targetURL, nil)
if err != nil {
masterlog.Error("Error creating request", map[string]interface{}{"error": err})
http.Error(w, "Failed to create request", http.StatusInternalServerError)
return
}
// Copy headers from original request
for key, values := range r.Header {
// Skip certain headers that shouldn't be forwarded
if strings.ToLower(key) == "host" {
continue
}
for _, value := range values {
req.Header.Add(key, value)
}
}
resp, err := client.Do(req)
if err != nil {
masterlog.Error("Error fetching target URL", map[string]interface{}{"error": err, "url": targetURL})
http.Error(w, "Not Found", http.StatusNotFound)
return
}
defer resp.Body.Close()
// Check if response is successful
if resp.StatusCode != http.StatusOK {
masterlog.Info("Target returned non-200 status", map[string]interface{}{
"url": targetURL,
"status_code": resp.StatusCode,
})
http.Error(w, "Not Found", http.StatusNotFound)
return
}
// Copy headers from target response
for key, values := range resp.Header {
for _, value := range values {
w.Header().Add(key, value)
}
}
// Set status code to 200
w.WriteHeader(http.StatusOK)
// Copy the response body
_, err = io.Copy(w, resp.Body)
if err != nil {
masterlog.Error("Error copying response", map[string]interface{}{"error": err})
// Can't send error response after headers have been written
return
}
masterlog.Info("Successfully proxied request", map[string]interface{}{
"url": targetURL,
"status_code": http.StatusOK,
"content_length": resp.ContentLength,
})
}