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