commit 20bfd8934789cd60cf2b9394f0a365c7d88c5d50 Author: JuLi0n21 Date: Tue Apr 28 14:20:52 2026 +0200 init diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f92b965 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM golang:1.26-alpine AS builder + +RUN apk --no-cache add ca-certificates + +WORKDIR /app +COPY . . + +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o shit-proxy . + +FROM scratch + +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY --from=builder /app/shit-proxy /shit-proxy + +EXPOSE 8080 +ENTRYPOINT ["/shit-proxy"] \ No newline at end of file diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..6908e25 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1776877367, + "narHash": "sha256-EHq1/OX139R1RvBzOJ0aMRT3xnWyqtHBRUBuO1gFzjI=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "0726a0ecb6d4e08f6adced58726b95db924cef57", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..4291950 --- /dev/null +++ b/flake.nix @@ -0,0 +1,19 @@ +{ + description = "Dev environment for shit-proxy"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + }; + + outputs = { self, nixpkgs, ... }: + let + pkgs = import nixpkgs { system = "x86_64-linux"; }; + in + { + devShells.x86_64-linux.default = pkgs.mkShell { + packages = with pkgs; [ + go_1_26 + ]; + }; + }; +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0ff887c --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module shit-proxy + +go 1.26.2 diff --git a/main.go b/main.go new file mode 100644 index 0000000..597f6c8 --- /dev/null +++ b/main.go @@ -0,0 +1,187 @@ +package main + +import ( + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "io" + "mime" + "net/http" + "net/url" + "os" + "path" + "path/filepath" + "strings" + "fmt" + "time" + "log" +) + +var allowedDomains []string + +func init() { + env := os.Getenv("ALLOWED_DOMAINS") + if env != "" { + parts := strings.Split(env, ",") + for _, p := range parts { + cleanDomain := strings.ToLower(strings.TrimSpace(p)) + if cleanDomain != "" { + allowedDomains = append(allowedDomains, cleanDomain) + } + } + } + + fmt.Println("allowed domains:", env) +} + +func main() { + http.HandleFunc("/", proxyHandler) + http.ListenAndServe(":8080", nil) +} + +func isDomainAllowed(host string) bool { + if len(allowedDomains) == 0 { + return true + } + + host = strings.ToLower(host) + for _, domain := range allowedDomains { + if host == domain || strings.HasSuffix(host, "."+domain) { + return true + } + } + return false +} + +func proxyHandler(w http.ResponseWriter, r *http.Request) { + start := time.Now() + encodedPath := strings.TrimPrefix(r.URL.Path, "/") + if encodedPath == "" { + http.Error(w, "Bad Request", http.StatusBadRequest) + return + } + + decodedBytes, err := base64.URLEncoding.DecodeString(encodedPath) + if err != nil { + decodedBytes, err = base64.RawURLEncoding.DecodeString(encodedPath) + if err != nil { + decodedBytes, err = base64.StdEncoding.DecodeString(encodedPath) + if err != nil { + decodedBytes, err = base64.RawStdEncoding.DecodeString(encodedPath) + if err != nil { + http.Error(w, "Invalid Base64", http.StatusBadRequest) + return + } + } + } + } + + targetURLStr := string(decodedBytes) + if !strings.HasPrefix(targetURLStr, "http://") && !strings.HasPrefix(targetURLStr, "https://") { + targetURLStr = "https://" + targetURLStr + } + + parsedTargetURL, err := url.Parse(targetURLStr) + if err != nil { + http.Error(w, "Invalid Target URL", http.StatusBadRequest) + return + } + + if !isDomainAllowed(parsedTargetURL.Hostname()) { + log.Println("blocked:", parsedTargetURL.Hostname()) + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + + fullRequestURL := targetURLStr + if r.URL.RawQuery != "" { + if strings.Contains(fullRequestURL, "?") { + fullRequestURL += "&" + r.URL.RawQuery + } else { + fullRequestURL += "?" + r.URL.RawQuery + } + } + + hasher := sha256.New() + hasher.Write([]byte(fullRequestURL)) + hashHex := hex.EncodeToString(hasher.Sum(nil)) + + matches, _ := filepath.Glob(hashHex + "*") + if len(matches) > 0 { + cachedFile := matches[0] + file, err := os.Open(cachedFile) + if err == nil { + defer file.Close() + ext := filepath.Ext(cachedFile) + if contentType := mime.TypeByExtension(ext); contentType != "" { + w.Header().Set("Content-Type", contentType) + } + io.Copy(w, file) + log.Println("cache hit | took:", time.Since(start)) + return + } + } + + proxyReq, err := http.NewRequest(r.Method, fullRequestURL, r.Body) + if err != nil { + http.Error(w, "Error creating request", http.StatusInternalServerError) + return + } + + for name, values := range r.Header { + for _, value := range values { + proxyReq.Header.Add(name, value) + } + } + proxyReq.Host = proxyReq.URL.Host + proxyReq.Header.Del("Accept-Encoding") + + client := &http.Client{} + resp, err := client.Do(proxyReq) + if err != nil { + http.Error(w, "Bad Gateway", http.StatusBadGateway) + return + } + defer resp.Body.Close() + + for name, values := range resp.Header { + for _, value := range values { + w.Header().Add(name, value) + } + } + w.WriteHeader(resp.StatusCode) + + if resp.StatusCode != http.StatusOK { + io.Copy(w, resp.Body) + return + } + + originalExt := path.Ext(parsedTargetURL.Path) + ext := originalExt + + if ext == "" { + contentType := resp.Header.Get("Content-Type") + if contentType != "" { + mediaType, _, err := mime.ParseMediaType(contentType) + if err == nil { + exts, err := mime.ExtensionsByType(mediaType) + if err == nil && len(exts) > 0 { + ext = exts[0] + } + } + } + } + + safeFileName := hashHex + ext + + file, err := os.Create(safeFileName) + if err != nil { + io.Copy(w, resp.Body) + return + } + defer file.Close() + log.Println("cache miss | took:", time.Since(start)) + + tee := io.TeeReader(resp.Body, file) + io.Copy(w, tee) +} \ No newline at end of file