]> Cypherpunks.ru repositories - gocheese.git/blob - main.go
BLAKE2b-256-aware -fsck
[gocheese.git] / main.go
1 /*
2 GoCheese -- Python private package repository and caching proxy
3 Copyright (C) 2019-2021 Sergey Matveev <stargrave@stargrave.org>
4               2019-2021 Elena Balakhonova <balakhonova_e@riseup.net>
5
6 This program is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, version 3 of the License.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 // Python private package repository and caching proxy
20 package main
21
22 import (
23         "bytes"
24         "context"
25         "crypto/sha256"
26         "crypto/tls"
27         "encoding/hex"
28         "errors"
29         "flag"
30         "fmt"
31         "log"
32         "net"
33         "net/http"
34         "net/url"
35         "os"
36         "os/signal"
37         "path/filepath"
38         "runtime"
39         "strings"
40         "syscall"
41         "time"
42
43         "golang.org/x/net/netutil"
44 )
45
46 const (
47         Version   = "3.0.0"
48         UserAgent = "GoCheese/" + Version
49
50         Warranty = `This program is free software: you can redistribute it and/or modify
51 it under the terms of the GNU General Public License as published by
52 the Free Software Foundation, version 3 of the License.
53
54 This program is distributed in the hope that it will be useful,
55 but WITHOUT ANY WARRANTY; without even the implied warranty of
56 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
57 GNU General Public License for more details.
58
59 You should have received a copy of the GNU General Public License
60 along with this program.  If not, see <http://www.gnu.org/licenses/>.`
61 )
62
63 var (
64         Root       = flag.String("root", "./packages", "Path to packages directory")
65         Bind       = flag.String("bind", "[::]:8080", "Address to bind to")
66         MaxClients = flag.Int("maxclients", 128, "Maximal amount of simultaneous clients")
67         DoUCSPI    = flag.Bool("ucspi", false, "Work as UCSPI-TCP service")
68
69         TLSCert = flag.String("tls-cert", "", "Path to TLS X.509 certificate")
70         TLSKey  = flag.String("tls-key", "", "Path to TLS X.509 private key")
71
72         NoRefreshURLPath = flag.String("norefresh", "/norefresh/", "Non-refreshing URL path")
73         RefreshURLPath   = flag.String("refresh", "/simple/", "Auto-refreshing URL path")
74         GPGUpdateURLPath = flag.String("gpgupdate", "/gpgupdate/", "GPG forceful refreshing URL path")
75         JSONURLPath      = flag.String("json", "/pypi/", "JSON API URL path")
76
77         PyPIURL      = flag.String("pypi", "https://pypi.org/simple/", "Upstream (PyPI) URL")
78         PyPICertHash = flag.String("pypi-cert-hash", "", "Authenticate upstream by its X.509 certificate's SPKI SHA256 hash")
79         JSONURL      = flag.String("pypi-json", "https://pypi.org/pypi/", "Enable and use specified JSON API upstream URL")
80
81         PasswdPath     = flag.String("passwd", "", "Path to FIFO for upload authentication")
82         PasswdListPath = flag.String("passwd-list", "", "Path to FIFO for login listing")
83         PasswdCheck    = flag.Bool("passwd-check", false, "Run password checker")
84
85         LogTimestamped = flag.Bool("log-timestamped", false, "Prepend timestmap to log messages")
86         FSCK           = flag.Bool("fsck", false, "Check integrity of all packages (errors are in stderr)")
87         DoVersion      = flag.Bool("version", false, "Print version information")
88         DoWarranty     = flag.Bool("warranty", false, "Print warranty information")
89
90         Killed bool
91 )
92
93 func servePkg(w http.ResponseWriter, r *http.Request, pkgName, filename string) {
94         log.Println(r.RemoteAddr, "get", filename)
95         path := filepath.Join(*Root, pkgName, filename)
96         if _, err := os.Stat(path); os.IsNotExist(err) {
97                 if !refreshDir(w, r, pkgName, filename, false) {
98                         return
99                 }
100         }
101         http.ServeFile(w, r, path)
102 }
103
104 func handler(w http.ResponseWriter, r *http.Request) {
105         w.Header().Set("Server", UserAgent)
106         switch r.Method {
107         case "GET":
108                 var path string
109                 var autorefresh bool
110                 var gpgUpdate bool
111                 if strings.HasPrefix(r.URL.Path, *NoRefreshURLPath) {
112                         path = strings.TrimPrefix(r.URL.Path, *NoRefreshURLPath)
113                 } else if strings.HasPrefix(r.URL.Path, *RefreshURLPath) {
114                         path = strings.TrimPrefix(r.URL.Path, *RefreshURLPath)
115                         autorefresh = true
116                 } else if strings.HasPrefix(r.URL.Path, *GPGUpdateURLPath) {
117                         path = strings.TrimPrefix(r.URL.Path, *GPGUpdateURLPath)
118                         autorefresh = true
119                         gpgUpdate = true
120                 } else {
121                         http.Error(w, "unknown action", http.StatusBadRequest)
122                         return
123                 }
124                 parts := strings.Split(strings.TrimSuffix(path, "/"), "/")
125                 if len(parts) > 2 {
126                         http.Error(w, "invalid path", http.StatusBadRequest)
127                         return
128                 }
129                 if len(parts) == 1 {
130                         if parts[0] == "" {
131                                 listRoot(w, r)
132                         } else {
133                                 serveListDir(w, r, parts[0], autorefresh, gpgUpdate)
134                         }
135                 } else {
136                         servePkg(w, r, parts[0], parts[1])
137                 }
138         case "POST":
139                 serveUpload(w, r)
140         default:
141                 http.Error(w, "unknown action", http.StatusBadRequest)
142         }
143 }
144
145 func main() {
146         flag.Parse()
147         if *DoWarranty {
148                 fmt.Println(Warranty)
149                 return
150         }
151         if *DoVersion {
152                 fmt.Println("GoCheese", Version, "built with", runtime.Version())
153                 return
154         }
155
156         if *LogTimestamped {
157                 log.SetFlags(log.Ldate | log.Lmicroseconds | log.Lshortfile)
158         } else {
159                 log.SetFlags(log.Lshortfile)
160         }
161         if !*DoUCSPI {
162                 log.SetOutput(os.Stdout)
163         }
164
165         if *FSCK {
166                 if !goodIntegrity() {
167                         os.Exit(1)
168                 }
169                 return
170         }
171
172         if *PasswdCheck {
173                 if passwdReader(os.Stdin) {
174                         os.Exit(0)
175                 } else {
176                         os.Exit(1)
177                 }
178         }
179
180         if *PasswdPath != "" {
181                 go func() {
182                         for {
183                                 fd, err := os.OpenFile(
184                                         *PasswdPath,
185                                         os.O_RDONLY,
186                                         os.FileMode(0666),
187                                 )
188                                 if err != nil {
189                                         log.Fatalln(err)
190                                 }
191                                 passwdReader(fd)
192                                 fd.Close()
193                         }
194                 }()
195         }
196         if *PasswdListPath != "" {
197                 go func() {
198                         for {
199                                 fd, err := os.OpenFile(
200                                         *PasswdListPath,
201                                         os.O_WRONLY|os.O_APPEND,
202                                         os.FileMode(0666),
203                                 )
204                                 if err != nil {
205                                         log.Fatalln(err)
206                                 }
207                                 passwdLister(fd)
208                                 fd.Close()
209                         }
210                 }()
211         }
212
213         if (*TLSCert != "" && *TLSKey == "") || (*TLSCert == "" && *TLSKey != "") {
214                 log.Fatalln("Both -tls-cert and -tls-key are required")
215         }
216
217         var err error
218         PyPIURLParsed, err = url.Parse(*PyPIURL)
219         if err != nil {
220                 log.Fatalln(err)
221         }
222         tlsConfig := tls.Config{
223                 ClientSessionCache: tls.NewLRUClientSessionCache(16),
224                 NextProtos:         []string{"h2", "http/1.1"},
225         }
226         PyPIHTTPTransport = http.Transport{
227                 ForceAttemptHTTP2: true,
228                 TLSClientConfig:   &tlsConfig,
229         }
230         if *PyPICertHash != "" {
231                 ourDgst, err := hex.DecodeString(*PyPICertHash)
232                 if err != nil {
233                         log.Fatalln(err)
234                 }
235                 tlsConfig.VerifyConnection = func(s tls.ConnectionState) error {
236                         spki := s.VerifiedChains[0][0].RawSubjectPublicKeyInfo
237                         theirDgst := sha256.Sum256(spki)
238                         if bytes.Compare(ourDgst, theirDgst[:]) != 0 {
239                                 return errors.New("certificate's SPKI digest mismatch")
240                         }
241                         return nil
242                 }
243         }
244
245         server := &http.Server{
246                 ReadTimeout:  time.Minute,
247                 WriteTimeout: time.Minute,
248         }
249         http.HandleFunc("/", serveHRRoot)
250         http.HandleFunc("/hr/", serveHRPkg)
251         http.HandleFunc(*JSONURLPath, serveJSON)
252         http.HandleFunc(*NoRefreshURLPath, handler)
253         http.HandleFunc(*RefreshURLPath, handler)
254         if *GPGUpdateURLPath != "" {
255                 http.HandleFunc(*GPGUpdateURLPath, handler)
256         }
257
258         if *DoUCSPI {
259                 server.SetKeepAlivesEnabled(false)
260                 ln := &UCSPI{}
261                 server.ConnState = connStater
262                 err := server.Serve(ln)
263                 if _, ok := err.(UCSPIAlreadyAccepted); !ok {
264                         log.Fatalln(err)
265                 }
266                 UCSPIJob.Wait()
267                 return
268         }
269
270         ln, err := net.Listen("tcp", *Bind)
271         if err != nil {
272                 log.Fatal(err)
273         }
274         ln = netutil.LimitListener(ln, *MaxClients)
275
276         needsShutdown := make(chan os.Signal, 0)
277         exitErr := make(chan error, 0)
278         signal.Notify(needsShutdown, syscall.SIGTERM, syscall.SIGINT)
279         go func(s *http.Server) {
280                 <-needsShutdown
281                 Killed = true
282                 log.Println("shutting down")
283                 ctx, cancel := context.WithTimeout(context.TODO(), time.Minute)
284                 exitErr <- s.Shutdown(ctx)
285                 cancel()
286         }(server)
287
288         log.Println(
289                 UserAgent, "ready:",
290                 "root:", *Root,
291                 "bind:", *Bind,
292                 "pypi:", *PyPIURL,
293                 "json:", *JSONURL,
294                 "hr: /",
295         )
296         if *TLSCert == "" {
297                 err = server.Serve(ln)
298         } else {
299                 err = server.ServeTLS(ln, *TLSCert, *TLSKey)
300         }
301         if err != http.ErrServerClosed {
302                 log.Fatal(err)
303         }
304         if err := <-exitErr; err != nil {
305                 log.Fatal(err)
306         }
307 }