package main import ( "context" "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "io" "log" "net" "net/http" "os" "os/signal" "path" "strings" "sync" "syscall" "time" ) var ( S3_HOST string S3_SECRET_KEY = EnvString("S3_SECRET_KEY", "xyz") S3_ACCESS_KEY = EnvString("S3_ACCESS_KEY", "123") S3_ENDPOINT = EnvString("S3_ENDPOINT", "https://bucket.s3.region.host.tld") S3_REGION = EnvString("S3_REGION", "region") S3_BUCKET = EnvString("S3_BUCKET", "bucket") HTTP_KEY = EnvString("HTTP_KEY", "teto") HTTP_ADDRESS = EnvString("HTTP_ADDRESS", "localhost:8080") HTTP_CORS_ORIGIN = EnvString("HTTP_CORS_ORIGIN", "") ) // ---------------------------------------------------------------------------- // HELPER FUNCTIONS // ---------------------------------------------------------------------------- func EnvString(field, initial string) string { if value := os.Getenv(field); value == "" { return initial } else { return value } } func SHA256HEX(data []byte) string { h := sha256.Sum256(data) return hex.EncodeToString(h[:]) } func SHA256HMAC(key, data []byte) []byte { h := hmac.New(sha256.New, key) h.Write(data) return h.Sum(nil) } func ValidateToken(r *http.Request) bool { requestPath := r.URL.Path requestToken := r.URL.Query().Get("token") if requestToken == "" { return false } // Decode Token Segments s := strings.SplitN(requestToken, ".", 2) if len(s) != 2 { return false } payload, err := base64.RawURLEncoding.DecodeString(s[0]) if err != nil { return false } signature, err := base64.RawURLEncoding.DecodeString(s[1]) if err != nil { return false } // Validate Token Signature hash := hmac.New(sha256.New, []byte(HTTP_KEY)) hash.Write(payload) if !hmac.Equal(signature, hash.Sum(nil)) { return false } // Parse Token Data var TokenData struct { Pattern string `json:"pattern"` Expires int64 `json:"expires"` } if err := json.Unmarshal(payload, &TokenData); err != nil { return false } if time.Now().Unix() > TokenData.Expires { return false } if ok, err := path.Match(TokenData.Pattern, requestPath); err != nil || !ok { return false } // LGTM return true } func SignRequest(r *http.Request) { // Timestamp t := time.Now().UTC() dateStamp := t.Format("20060102") dataAmazon := t.Format("20060102T150405Z") // Hash Content payload := []byte{} payloadHashHexSHA := SHA256HEX(payload) // Apply Headers r.Header.Set("Host", S3_HOST) r.Header.Set("x-amz-date", dataAmazon) r.Header.Set("x-amz-content-sha256", payloadHashHexSHA) r.Host = S3_HOST // 1. Create a canonical request signedHeaders := "host;x-amz-content-sha256;x-amz-date" canonicalHeaders := fmt.Sprintf( "host:%s\nx-amz-content-sha256:%s\nx-amz-date:%s\n", r.Host, payloadHashHexSHA, dataAmazon, ) canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", r.Method, r.URL.EscapedPath(), r.URL.Query().Encode(), canonicalHeaders, signedHeaders, payloadHashHexSHA, ) // Step 2: String to sign service := "s3" scope := fmt.Sprintf("%s/%s/%s/aws4_request", dateStamp, S3_REGION, service) stringToSign := fmt.Sprintf("AWS4-HMAC-SHA256\n%s\n%s\n%s", dataAmazon, scope, SHA256HEX([]byte(canonicalRequest)), ) // Step 3: Derive signing key kDate := SHA256HMAC([]byte("AWS4"+S3_SECRET_KEY), []byte(dateStamp)) kRegion := SHA256HMAC(kDate, []byte(S3_REGION)) kService := SHA256HMAC(kRegion, []byte(service)) kSigning := SHA256HMAC(kService, []byte("aws4_request")) // Step 4: Signature signature := hex.EncodeToString(SHA256HMAC(kSigning, []byte(stringToSign))) authHeader := fmt.Sprintf( "AWS4-HMAC-SHA256 Credential=%s/%s, SignedHeaders=%s, Signature=%s", S3_ACCESS_KEY, scope, signedHeaders, signature, ) r.Header.Set("Authorization", authHeader) } // ---------------------------------------------------------------------------- // SERVICE FUNCTIONS // ---------------------------------------------------------------------------- func SetupMux() *http.ServeMux { mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { // Handle CORS Requests if HTTP_CORS_ORIGIN != "" { w.Header().Add("Access-Control-Allow-Origin", HTTP_CORS_ORIGIN) w.Header().Add("Access-Control-Allow-Methods", "GET, OPTIONS") w.Header().Add("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusNoContent) return } } // Request Validation if r.Method != "GET" { w.WriteHeader(http.StatusMethodNotAllowed) return } if r.URL.Path == "/" { w.WriteHeader(http.StatusNotFound) return } // Check Access Token for Private Endpoints if strings.HasPrefix(r.URL.Path, "/private") { w.Header().Add("Cache-Control", "no-store") if !ValidateToken(r) { w.WriteHeader(http.StatusUnauthorized) return } } // Request Bucket File url := fmt.Sprint(S3_ENDPOINT, r.URL.Path) req, err := http.NewRequest(http.MethodGet, url, http.NoBody) if err != nil { return } SignRequest(req) res, err := http.DefaultClient.Do(req) if err != nil { http.Error(w, "Upstream Error", http.StatusBadGateway) return } defer res.Body.Close() // Stream File Contents w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") w.Header().Set("Content-Type", res.Header.Get("Content-Type")) w.WriteHeader(res.StatusCode) io.Copy(w, res.Body) }) return mux } func StartupHTTP(stop context.Context, await *sync.WaitGroup) { var listener net.Listener var err error if strings.HasPrefix(HTTP_ADDRESS, "unix/") { path := strings.TrimPrefix(HTTP_ADDRESS, "unix/") os.Remove(path) listener, err = net.Listen("unix", path) if err == nil { os.Chmod(path, 0660) } } else { listener, err = net.Listen("tcp", HTTP_ADDRESS) } if err != nil { log.Fatalf("Listen Failed: %s\n", err) return } svr := http.Server{ Handler: SetupMux(), MaxHeaderBytes: 4096, IdleTimeout: 10 * time.Second, ReadHeaderTimeout: 10 * time.Second, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, } // Shutdown Logic await.Add(1) go func() { defer await.Done() <-stop.Done() shutdownCtx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() if err := svr.Shutdown(shutdownCtx); err != nil { log.Printf("Shutdown Error: %s\n", err) return } log.Println("Server Closed") }() log.Printf("Listening @ %s\n", HTTP_ADDRESS) if err := svr.Serve(listener); err != http.ErrServerClosed { log.Printf("Startup Failed: %s\n", err) return } } func main() { time.Local = time.UTC // Startup Services var stopCtx, stop = context.WithCancel(context.Background()) var stopWg sync.WaitGroup go StartupHTTP(stopCtx, &stopWg) // Await Shutdown Signal cancel := make(chan os.Signal, 1) signal.Notify(cancel, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) <-cancel stop() // Begin Shutdown Process log.Println("Shutting Down!") timeout, finish := context.WithTimeout(context.Background(), time.Minute) defer finish() go func() { <-timeout.Done() if timeout.Err() == context.DeadlineExceeded { log.Fatalln("Shutdown Deadline Exceeded") return } }() stopWg.Wait() os.Exit(0) }