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>
This commit is contained in:
150
app/main.go
Normal file
150
app/main.go
Normal file
@@ -0,0 +1,150 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user