blob: 9dd0d0edb16ad3442e3a6bbc932c58e56548860f [file] [log] [blame]
Tim Windelschmidt5178dd72024-12-04 04:38:45 +01001package main
2
3import (
4 "context"
5 "errors"
6 "flag"
7 "fmt"
8 "io"
9 "log"
10 "net/http"
11 "strings"
12
13 "cloud.google.com/go/storage"
14 "google.golang.org/api/option"
15)
16
17var (
18 flagUser string
19 flagPass string
20 flagMirrorBucketName string
21 flagCredentialsFile string
22)
23
24func main() {
25 flag.StringVar(&flagUser, "username", "", "Username required to enable s3 upload")
26 flag.StringVar(&flagPass, "password", "", "Password required to enable s3 upload")
27 flag.StringVar(&flagCredentialsFile, "credentials_file", "", "Credentials file to use for GCS")
28 flag.StringVar(&flagMirrorBucketName, "bucket_name", "monogon-bazel-mirror", "Name of GCS bucket to mirror into.")
29 flag.Parse()
30
31 if flagUser == "" || flagPass == "" {
32 log.Fatalf("Missing username or password flag")
33 }
34
35 if flagCredentialsFile == "" {
36 log.Fatalf("Missing credentials flag")
37 }
38
39 client, err := storage.NewClient(context.Background(), option.WithCredentialsFile(flagCredentialsFile))
40 if err != nil {
41 log.Fatalf("Could not build google cloud storage client: %v", err)
42 }
43
44 bucketClient := client.Bucket(flagMirrorBucketName)
45 handlerFunc := func(w http.ResponseWriter, r *http.Request) {
46 mirrorHandler(bucketClient, w, r)
47 }
48
49 log.Panic(http.ListenAndServe(":80", http.HandlerFunc(handlerFunc)))
50}
51
52func mirrorHandler(m *storage.BucketHandle, w http.ResponseWriter, r *http.Request) {
53 targetPath := strings.TrimPrefix(r.URL.Path, "/")
54 targetURL := "https://" + targetPath
55 if len(r.URL.Query()) != 0 {
56 targetURL += "?" + r.URL.Query().Encode()
57 }
58
59 if r.Method != http.MethodGet {
60 log.Printf("%s: invalid method %q: %v", r.RemoteAddr, targetURL, r.Method)
61 http.Error(w, "invalid method", http.StatusMethodNotAllowed)
62 return
63 }
64
65 if len(r.URL.Query()) != 0 {
66 log.Printf("%s: invalid query url: %q", r.RemoteAddr, targetURL)
67 http.Error(w, "URLs with query parameters are not supported", http.StatusNotAcceptable)
68 return
69 }
70
71 obj := m.Object(targetPath)
72 objR, err := obj.NewReader(r.Context())
73 if err != nil && !errors.Is(err, storage.ErrObjectNotExist) {
74 log.Printf("%s: fetching %q from bucket: %v", r.RemoteAddr, obj.ObjectName(), err)
75 http.Error(w, "internal server error", http.StatusInternalServerError)
76 return
77 }
78
79 // If not found and not authenticated, return 404
80 if errors.Is(err, storage.ErrObjectNotExist) && !isAuthenticated(r) {
81 http.Error(w, "object not found in mirror", http.StatusNotFound)
82 return
83 }
84
85 // If found, return mirror content
86 if err == nil {
87 log.Printf("%s: serving cached object %q", r.RemoteAddr, targetURL)
88
89 w.Header().Set("Content-Type", objR.Attrs.ContentType)
90 w.Header().Set("Content-Length", fmt.Sprintf("%d", objR.Attrs.Size))
91 w.WriteHeader(http.StatusOK)
92
93 _, _ = io.Copy(w, objR)
94 return
95 }
96
97 // If I am not reading the logic wrong, this should not happen, but
98 // better to be sure.
99 if !isAuthenticated(r) {
100 http.Error(w, "upstream fetch requires authentication", http.StatusUnauthorized)
101 return
102 }
103
104 // If not found, try download.
105 outReq, err := http.NewRequest(r.Method, targetURL, r.Body)
106 if err != nil {
107 log.Printf("%s: forwarding to %q failed: %v", r.RemoteAddr, targetURL, err)
108 http.Error(w, "internal server error", http.StatusInternalServerError)
109 return
110 }
111
112 copyHeader(outReq.Header, r.Header)
113 outReq.Header.Del("Authorization") // Don't forward our basic auth
114
115 res, err := http.DefaultClient.Do(outReq)
116 if err != nil {
117 log.Printf("%s: forwarding to %q failed: %v", r.RemoteAddr, targetURL, err)
118 http.Error(w, "could not reach endpoint", http.StatusBadGateway)
119 return
120 }
121 defer res.Body.Close()
122
123 // If not StatusOK, return upstream error
124 if res.StatusCode != http.StatusOK {
125 log.Printf("%s: serving upstream error %q: %s", r.RemoteAddr, targetURL, res.Status)
126
127 copyHeader(w.Header(), res.Header)
128 w.WriteHeader(res.StatusCode)
129
130 _, _ = io.Copy(w, res.Body)
131 return
132 }
133
134 var outW io.Writer = w
135 if objR == nil {
136 // If not exist and authenticated, create
137
138 log.Printf("%s: populating object %q", r.RemoteAddr, targetURL)
139 objW := obj.If(storage.Conditions{DoesNotExist: true}).NewWriter(r.Context())
140 defer objW.Close()
141
142 outW = io.MultiWriter(outW, objW)
143 } else if res.ContentLength != -1 && res.ContentLength != objR.Attrs.Size {
144 // If diff and authenticated, update
145
146 log.Printf("%s: replacing object %q: size differs (orig, mirror) %d != %d", r.RemoteAddr, targetURL, res.ContentLength, objR.Attrs.Size)
147 objW := obj.If(storage.Conditions{GenerationMatch: objR.Attrs.Generation}).NewWriter(r.Context())
148 defer objW.Close()
149
150 outW = io.MultiWriter(outW, objW)
151 } else {
152 // If same and authenticated, return cached
153 log.Printf("%s: serving cached object %q", r.RemoteAddr, targetURL)
154
155 w.Header().Set("Content-Type", objR.Attrs.ContentType)
156 w.Header().Set("Content-Length", fmt.Sprintf("%d", objR.Attrs.Size))
157 w.WriteHeader(http.StatusOK)
158
159 _, _ = io.Copy(w, objR)
160 return
161 }
162
163 copyHeader(w.Header(), res.Header)
164 w.WriteHeader(res.StatusCode)
165
166 _, _ = io.Copy(outW, res.Body)
167}
168
169func isAuthenticated(r *http.Request) bool {
170 user, pass, ok := r.BasicAuth()
171 if !ok {
172 return false
173 }
174
175 return user == flagUser && pass == flagPass
176}
177
178func copyHeader(dst, src http.Header) {
179 for k, vv := range src {
180 for _, v := range vv {
181 dst.Add(k, v)
182 }
183 }
184}