]> Cypherpunks.ru repositories - gocheese.git/blob - gocheese.go
Multiple upstream digest algorithms support
[gocheese.git] / gocheese.go
1 /*
2 GoCheese -- Python private package repository and caching proxy
3 Copyright (C) 2019 Sergey Matveev <stargrave@stargrave.org>
4               2019 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         "bufio"
24         "bytes"
25         "context"
26         "crypto/md5"
27         "crypto/sha256"
28         "crypto/sha512"
29         "encoding/hex"
30         "flag"
31         "fmt"
32         "hash"
33         "io"
34         "io/ioutil"
35         "log"
36         "net"
37         "net/http"
38         "net/url"
39         "os"
40         "os/signal"
41         "path/filepath"
42         "regexp"
43         "runtime"
44         "strings"
45         "syscall"
46         "time"
47
48         "golang.org/x/net/netutil"
49 )
50
51 const (
52         HTMLBegin = `<!DOCTYPE html>
53 <html>
54   <head>
55     <title>Links for %s</title>
56   </head>
57   <body>
58 `
59         HTMLEnd      = "  </body>\n</html>\n"
60         HTMLElement  = "    <a href=\"%s\"%s>%s</a><br/>\n"
61         InternalFlag = ".internal"
62         GPGSigExt    = ".asc"
63         GPGSigAttr   = " data-gpg-sig=true"
64
65         Warranty = `This program is free software: you can redistribute it and/or modify
66 it under the terms of the GNU General Public License as published by
67 the Free Software Foundation, version 3 of the License.
68
69 This program is distributed in the hope that it will be useful,
70 but WITHOUT ANY WARRANTY; without even the implied warranty of
71 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
72 GNU General Public License for more details.
73
74 You should have received a copy of the GNU General Public License
75 along with this program.  If not, see <http://www.gnu.org/licenses/>.`
76 )
77
78 var (
79         pkgPyPI        = regexp.MustCompile(`^.*<a href="([^"]+)"[^>]*>(.+)</a><br/>.*$`)
80         Version string = "UNKNOWN"
81
82         root             = flag.String("root", "./packages", "Path to packages directory")
83         bind             = flag.String("bind", "[::]:8080", "Address to bind to")
84         tlsCert          = flag.String("tls-cert", "", "Path to TLS X.509 certificate")
85         tlsKey           = flag.String("tls-key", "", "Path to TLS X.509 private key")
86         norefreshURLPath = flag.String("norefresh", "/norefresh/", "Non-refreshing URL path")
87         refreshURLPath   = flag.String("refresh", "/simple/", "Auto-refreshing URL path")
88         gpgUpdateURLPath = flag.String("gpgupdate", "/gpgupdate/", "GPG forceful refreshing URL path")
89         pypiURL          = flag.String("pypi", "https://pypi.org/simple/", "Upstream PyPI URL")
90         passwdPath       = flag.String("passwd", "passwd", "Path to file with authenticators")
91         passwdCheck      = flag.Bool("passwd-check", false, "Test the -passwd file for syntax errors and exit")
92         fsck             = flag.Bool("fsck", false, "Check integrity of all packages")
93         maxClients       = flag.Int("maxclients", 128, "Maximal amount of simultaneous clients")
94         version          = flag.Bool("version", false, "Print version information")
95         warranty         = flag.Bool("warranty", false, "Print warranty information")
96
97         killed bool
98
99         pypiURLParsed   *url.URL
100         normalizationRe *regexp.Regexp = regexp.MustCompilePOSIX("[-_.]+")
101 )
102
103 func mkdirForPkg(w http.ResponseWriter, r *http.Request, dir string) bool {
104         path := filepath.Join(*root, dir)
105         if _, err := os.Stat(path); os.IsNotExist(err) {
106                 if err = os.Mkdir(path, os.FileMode(0777)); err != nil {
107                         http.Error(w, err.Error(), http.StatusInternalServerError)
108                         return false
109                 }
110                 log.Println(r.RemoteAddr, "mkdir", dir)
111         }
112         return true
113 }
114
115 func refreshDir(
116         w http.ResponseWriter,
117         r *http.Request,
118         dir,
119         filenameGet string,
120         gpgUpdate bool,
121 ) bool {
122         if _, err := os.Stat(filepath.Join(*root, dir, InternalFlag)); err == nil {
123                 return true
124         }
125         resp, err := http.Get(*pypiURL + dir + "/")
126         if err != nil {
127                 http.Error(w, err.Error(), http.StatusBadGateway)
128                 return false
129         }
130         body, err := ioutil.ReadAll(resp.Body)
131         resp.Body.Close()
132         if err != nil {
133                 http.Error(w, err.Error(), http.StatusBadGateway)
134                 return false
135         }
136         if !mkdirForPkg(w, r, dir) {
137                 return false
138         }
139         dirPath := filepath.Join(*root, dir)
140         var submatches []string
141         var uri string
142         var filename string
143         var path string
144         var pkgURL *url.URL
145         var digest []byte
146         for _, lineRaw := range bytes.Split(body, []byte("\n")) {
147                 submatches = pkgPyPI.FindStringSubmatch(string(lineRaw))
148                 if len(submatches) == 0 {
149                         continue
150                 }
151                 uri = submatches[1]
152                 filename = submatches[2]
153                 if pkgURL, err = url.Parse(uri); err != nil {
154                         http.Error(w, err.Error(), http.StatusBadGateway)
155                         return false
156                 }
157
158                 if pkgURL.Fragment == "" {
159                         log.Println(r.RemoteAddr, "pypi", filename, "no digest provided")
160                         http.Error(w, "no digest provided", http.StatusBadGateway)
161                         return false
162                 }
163                 digestInfo := strings.Split(pkgURL.Fragment, "=")
164                 if len(digestInfo) == 1 {
165                         // Ancient non PEP-0503 PyPIs, assume MD5
166                         digestInfo = []string{"md5", digestInfo[0]}
167                 } else if len(digestInfo) != 2 {
168                         log.Println(r.RemoteAddr, "pypi", filename, "invalid digest provided")
169                         http.Error(w, "invalid digest provided", http.StatusBadGateway)
170                         return false
171                 }
172                 digest, err = hex.DecodeString(digestInfo[1])
173                 if err != nil {
174                         http.Error(w, err.Error(), http.StatusBadGateway)
175                         return false
176                 }
177                 var hasherNew func() hash.Hash
178                 var hashExt string
179                 var hashSize int
180                 switch digestInfo[0] {
181                 case "md5":
182                         hashExt = ".md5"
183                         hasherNew = md5.New
184                         hashSize = md5.Size
185                 case "sha256":
186                         hashExt = ".sha256"
187                         hasherNew = sha256.New
188                         hashSize = sha256.Size
189                 case "sha512":
190                         hashExt = ".sha512"
191                         hasherNew = sha512.New
192                         hashSize = sha512.Size
193                 default:
194                         log.Println(
195                                 r.RemoteAddr, "pypi", filename,
196                                 "unknown digest algorithm", digestInfo[0],
197                         )
198                         http.Error(w, "unknown digest algorithm", http.StatusBadGateway)
199                         return false
200                 }
201                 if len(digest) != hashSize {
202                         log.Println(r.RemoteAddr, "pypi", filename, "invalid digest length")
203                         http.Error(w, "invalid digest length", http.StatusBadGateway)
204                         return false
205                 }
206
207                 pkgURL.Fragment = ""
208                 if pkgURL.Host == "" {
209                         uri = pypiURLParsed.ResolveReference(pkgURL).String()
210                 } else {
211                         uri = pkgURL.String()
212                 }
213                 path = filepath.Join(dirPath, filename)
214                 if filename == filenameGet {
215                         if killed {
216                                 // Skip heavy remote call, when shutting down
217                                 http.Error(w, "shutting down", http.StatusInternalServerError)
218                                 return false
219                         }
220                         log.Println(r.RemoteAddr, "pypi download", filename)
221                         resp, err = http.Get(uri)
222                         if err != nil {
223                                 log.Println(r.RemoteAddr, "pypi download error:", err.Error())
224                                 http.Error(w, err.Error(), http.StatusBadGateway)
225                                 return false
226                         }
227                         defer resp.Body.Close()
228                         hasher := hasherNew()
229                         hasherOur := sha256.New()
230                         dst, err := TempFile(dirPath)
231                         if err != nil {
232                                 http.Error(w, err.Error(), http.StatusInternalServerError)
233                                 return false
234                         }
235                         dstBuf := bufio.NewWriter(dst)
236                         wrs := []io.Writer{hasher, dstBuf}
237                         if hashExt != ".sha256" {
238                                 wrs = append(wrs, hasherOur)
239                         }
240                         wr := io.MultiWriter(wrs...)
241                         if _, err = io.Copy(wr, resp.Body); err != nil {
242                                 os.Remove(dst.Name())
243                                 dst.Close()
244                                 http.Error(w, err.Error(), http.StatusInternalServerError)
245                                 return false
246                         }
247                         if err = dstBuf.Flush(); err != nil {
248                                 os.Remove(dst.Name())
249                                 dst.Close()
250                                 http.Error(w, err.Error(), http.StatusInternalServerError)
251                                 return false
252                         }
253                         if bytes.Compare(hasher.Sum(nil), digest) != 0 {
254                                 log.Println(r.RemoteAddr, "pypi", filename, "digest mismatch")
255                                 os.Remove(dst.Name())
256                                 dst.Close()
257                                 http.Error(w, "digest mismatch", http.StatusBadGateway)
258                                 return false
259                         }
260                         if err = dst.Sync(); err != nil {
261                                 os.Remove(dst.Name())
262                                 dst.Close()
263                                 http.Error(w, err.Error(), http.StatusInternalServerError)
264                                 return false
265                         }
266                         if err = dst.Close(); err != nil {
267                                 http.Error(w, err.Error(), http.StatusInternalServerError)
268                                 return false
269                         }
270                         if err = os.Rename(dst.Name(), path); err != nil {
271                                 http.Error(w, err.Error(), http.StatusInternalServerError)
272                                 return false
273                         }
274                         if err = DirSync(dirPath); err != nil {
275                                 http.Error(w, err.Error(), http.StatusInternalServerError)
276                                 return false
277                         }
278                         if hashExt != ".sha256" {
279                                 hashExt = ".sha256"
280                                 digest = hasherOur.Sum(nil)
281                         }
282                 }
283                 if filename == filenameGet || gpgUpdate {
284                         if _, err = os.Stat(path); err != nil {
285                                 goto GPGSigSkip
286                         }
287                         resp, err := http.Get(uri + GPGSigExt)
288                         if err != nil {
289                                 goto GPGSigSkip
290                         }
291                         if resp.StatusCode != http.StatusOK {
292                                 resp.Body.Close()
293                                 goto GPGSigSkip
294                         }
295                         sig, err := ioutil.ReadAll(resp.Body)
296                         resp.Body.Close()
297                         if err != nil {
298                                 goto GPGSigSkip
299                         }
300                         if err = WriteFileSync(dirPath, path+GPGSigExt, sig); err != nil {
301                                 http.Error(w, err.Error(), http.StatusInternalServerError)
302                                 return false
303                         }
304                         log.Println(r.RemoteAddr, "pypi downloaded signature", filename)
305                 }
306         GPGSigSkip:
307                 path = path + hashExt
308                 _, err = os.Stat(path)
309                 if err == nil {
310                         continue
311                 }
312                 if !os.IsNotExist(err) {
313                         http.Error(w, err.Error(), http.StatusInternalServerError)
314                         return false
315                 }
316                 log.Println(r.RemoteAddr, "pypi touch", filename)
317                 if err = WriteFileSync(dirPath, path, digest); err != nil {
318                         http.Error(w, err.Error(), http.StatusInternalServerError)
319                         return false
320                 }
321         }
322         return true
323 }
324
325 func listRoot(w http.ResponseWriter, r *http.Request) {
326         files, err := ioutil.ReadDir(*root)
327         if err != nil {
328                 http.Error(w, err.Error(), http.StatusInternalServerError)
329                 return
330         }
331         var result bytes.Buffer
332         result.WriteString(fmt.Sprintf(HTMLBegin, "root"))
333         for _, file := range files {
334                 if file.Mode().IsDir() {
335                         result.WriteString(fmt.Sprintf(
336                                 HTMLElement,
337                                 *refreshURLPath+file.Name()+"/",
338                                 file.Name(),
339                         ))
340                 }
341         }
342         result.WriteString(HTMLEnd)
343         w.Write(result.Bytes())
344 }
345
346 func listDir(
347         w http.ResponseWriter,
348         r *http.Request,
349         dir string,
350         autorefresh,
351         gpgUpdate bool,
352 ) {
353         dirPath := filepath.Join(*root, dir)
354         if autorefresh {
355                 if !refreshDir(w, r, dir, "", gpgUpdate) {
356                         return
357                 }
358         } else if _, err := os.Stat(dirPath); os.IsNotExist(err) && !refreshDir(w, r, dir, "", false) {
359                 return
360         }
361         fis, err := ioutil.ReadDir(dirPath)
362         if err != nil {
363                 http.Error(w, err.Error(), http.StatusInternalServerError)
364                 return
365         }
366         var result bytes.Buffer
367         result.WriteString(fmt.Sprintf(HTMLBegin, dir))
368         var digest []byte
369         var gpgSigAttr string
370         var fnClean string
371         files := make(map[string]struct{}, len(fis)/2)
372         for _, fi := range fis {
373                 files[fi.Name()] = struct{}{}
374         }
375         for _, algoExt := range []string{".sha256", ".sha512", ".md5"} {
376                 for fn, _ := range files {
377                         if killed {
378                                 // Skip expensive I/O when shutting down
379                                 http.Error(w, "shutting down", http.StatusInternalServerError)
380                                 return
381                         }
382                         if !strings.HasSuffix(fn, algoExt) {
383                                 continue
384                         }
385                         digest, err = ioutil.ReadFile(filepath.Join(dirPath, fn))
386                         if err != nil {
387                                 http.Error(w, err.Error(), http.StatusInternalServerError)
388                                 return
389                         }
390                         fnClean = strings.TrimSuffix(fn, algoExt)
391                         if _, err = os.Stat(filepath.Join(dirPath, fnClean+GPGSigExt)); os.IsNotExist(err) {
392                                 gpgSigAttr = ""
393                         } else {
394                                 gpgSigAttr = GPGSigAttr
395                         }
396                         result.WriteString(fmt.Sprintf(
397                                 HTMLElement,
398                                 strings.Join([]string{
399                                         *refreshURLPath, dir, "/", fnClean,
400                                         "#", algoExt[1:], "=", hex.EncodeToString(digest),
401                                 }, ""),
402                                 gpgSigAttr,
403                                 fnClean,
404                         ))
405                         for _, n := range []string{
406                                 fnClean,
407                                 fnClean + GPGSigExt,
408                                 fnClean + ".sha256",
409                                 fnClean + ".sha512",
410                                 fnClean + ".md5",
411                         } {
412                                 delete(files, n)
413                         }
414                 }
415         }
416         result.WriteString(HTMLEnd)
417         w.Write(result.Bytes())
418 }
419
420 func servePkg(w http.ResponseWriter, r *http.Request, dir, filename string) {
421         log.Println(r.RemoteAddr, "get", filename)
422         path := filepath.Join(*root, dir, filename)
423         if _, err := os.Stat(path); os.IsNotExist(err) {
424                 if !refreshDir(w, r, dir, filename, false) {
425                         return
426                 }
427         }
428         http.ServeFile(w, r, path)
429 }
430
431 func serveUpload(w http.ResponseWriter, r *http.Request) {
432         // Authentication
433         username, password, ok := r.BasicAuth()
434         if !ok {
435                 log.Println(r.RemoteAddr, "unauthenticated", username)
436                 http.Error(w, "unauthenticated", http.StatusUnauthorized)
437                 return
438         }
439         auther, ok := passwords[username]
440         if !ok || !auther.Auth(password) {
441                 log.Println(r.RemoteAddr, "unauthenticated", username)
442                 http.Error(w, "unauthenticated", http.StatusUnauthorized)
443                 return
444         }
445
446         // Form parsing
447         var err error
448         if err = r.ParseMultipartForm(1 << 20); err != nil {
449                 http.Error(w, err.Error(), http.StatusBadRequest)
450                 return
451         }
452         pkgNames, exists := r.MultipartForm.Value["name"]
453         if !exists || len(pkgNames) != 1 {
454                 http.Error(w, "single name is expected in request", http.StatusBadRequest)
455                 return
456         }
457         dir := normalizationRe.ReplaceAllString(pkgNames[0], "-")
458         dirPath := filepath.Join(*root, dir)
459         var digestExpected []byte
460         if digestExpectedHex, exists := r.MultipartForm.Value["sha256_digest"]; exists {
461                 digestExpected, err = hex.DecodeString(digestExpectedHex[0])
462                 if err != nil {
463                         http.Error(w, "bad sha256_digest: "+err.Error(), http.StatusBadRequest)
464                         return
465                 }
466         }
467         gpgSigsExpected := make(map[string]struct{})
468
469         // Checking is it internal package
470         if _, err = os.Stat(filepath.Join(dirPath, InternalFlag)); err != nil {
471                 log.Println(r.RemoteAddr, "non-internal package", dir)
472                 http.Error(w, "unknown internal package", http.StatusUnauthorized)
473                 return
474         }
475
476         for _, file := range r.MultipartForm.File["content"] {
477                 filename := file.Filename
478                 gpgSigsExpected[filename+GPGSigExt] = struct{}{}
479                 log.Println(r.RemoteAddr, "put", filename, "by", username)
480                 path := filepath.Join(dirPath, filename)
481                 if _, err = os.Stat(path); err == nil {
482                         log.Println(r.RemoteAddr, "already exists", filename)
483                         http.Error(w, "already exists", http.StatusBadRequest)
484                         return
485                 }
486                 if !mkdirForPkg(w, r, dir) {
487                         return
488                 }
489                 src, err := file.Open()
490                 defer src.Close()
491                 if err != nil {
492                         http.Error(w, err.Error(), http.StatusInternalServerError)
493                         return
494                 }
495                 dst, err := TempFile(dirPath)
496                 if err != nil {
497                         http.Error(w, err.Error(), http.StatusInternalServerError)
498                         return
499                 }
500                 dstBuf := bufio.NewWriter(dst)
501                 hasher := sha256.New()
502                 wr := io.MultiWriter(hasher, dst)
503                 if _, err = io.Copy(wr, src); err != nil {
504                         os.Remove(dst.Name())
505                         dst.Close()
506                         http.Error(w, err.Error(), http.StatusInternalServerError)
507                         return
508                 }
509                 if err = dstBuf.Flush(); err != nil {
510                         os.Remove(dst.Name())
511                         dst.Close()
512                         http.Error(w, err.Error(), http.StatusInternalServerError)
513                         return
514                 }
515                 if err = dst.Sync(); err != nil {
516                         os.Remove(dst.Name())
517                         dst.Close()
518                         http.Error(w, err.Error(), http.StatusInternalServerError)
519                         return
520                 }
521                 dst.Close()
522                 digest := hasher.Sum(nil)
523                 if digestExpected != nil {
524                         if bytes.Compare(digestExpected, digest) == 0 {
525                                 log.Println(r.RemoteAddr, filename, "good checksum received")
526                         } else {
527                                 log.Println(r.RemoteAddr, filename, "bad checksum received")
528                                 http.Error(w, "bad checksum", http.StatusBadRequest)
529                                 os.Remove(dst.Name())
530                                 return
531                         }
532                 }
533                 if err = os.Rename(dst.Name(), path); err != nil {
534                         http.Error(w, err.Error(), http.StatusInternalServerError)
535                         return
536                 }
537                 if err = DirSync(dirPath); err != nil {
538                         http.Error(w, err.Error(), http.StatusInternalServerError)
539                         return
540                 }
541                 if err = WriteFileSync(dirPath, path+".sha256", digest); err != nil {
542                         http.Error(w, err.Error(), http.StatusInternalServerError)
543                         return
544                 }
545         }
546         for _, file := range r.MultipartForm.File["gpg_signature"] {
547                 filename := file.Filename
548                 if _, exists := gpgSigsExpected[filename]; !exists {
549                         http.Error(w, "unexpected GPG signature filename", http.StatusBadRequest)
550                         return
551                 }
552                 delete(gpgSigsExpected, filename)
553                 log.Println(r.RemoteAddr, "put", filename, "by", username)
554                 path := filepath.Join(dirPath, filename)
555                 if _, err = os.Stat(path); err == nil {
556                         log.Println(r.RemoteAddr, "already exists", filename)
557                         http.Error(w, "already exists", http.StatusBadRequest)
558                         return
559                 }
560                 src, err := file.Open()
561                 if err != nil {
562                         http.Error(w, err.Error(), http.StatusInternalServerError)
563                         return
564                 }
565                 sig, err := ioutil.ReadAll(src)
566                 src.Close()
567                 if err != nil {
568                         http.Error(w, err.Error(), http.StatusInternalServerError)
569                         return
570                 }
571                 if err = WriteFileSync(dirPath, path, sig); err != nil {
572                         http.Error(w, err.Error(), http.StatusInternalServerError)
573                         return
574                 }
575         }
576 }
577
578 func handler(w http.ResponseWriter, r *http.Request) {
579         switch r.Method {
580         case "GET":
581                 var path string
582                 var autorefresh bool
583                 var gpgUpdate bool
584                 if strings.HasPrefix(r.URL.Path, *norefreshURLPath) {
585                         path = strings.TrimPrefix(r.URL.Path, *norefreshURLPath)
586                 } else if strings.HasPrefix(r.URL.Path, *refreshURLPath) {
587                         path = strings.TrimPrefix(r.URL.Path, *refreshURLPath)
588                         autorefresh = true
589                 } else if strings.HasPrefix(r.URL.Path, *gpgUpdateURLPath) {
590                         path = strings.TrimPrefix(r.URL.Path, *gpgUpdateURLPath)
591                         autorefresh = true
592                         gpgUpdate = true
593                 } else {
594                         http.Error(w, "unknown action", http.StatusBadRequest)
595                         return
596                 }
597                 parts := strings.Split(strings.TrimSuffix(path, "/"), "/")
598                 if len(parts) > 2 {
599                         http.Error(w, "invalid path", http.StatusBadRequest)
600                         return
601                 }
602                 if len(parts) == 1 {
603                         if parts[0] == "" {
604                                 listRoot(w, r)
605                         } else {
606                                 listDir(w, r, parts[0], autorefresh, gpgUpdate)
607                         }
608                 } else {
609                         servePkg(w, r, parts[0], parts[1])
610                 }
611         case "POST":
612                 serveUpload(w, r)
613         default:
614                 http.Error(w, "unknown action", http.StatusBadRequest)
615         }
616 }
617
618 func goodIntegrity() bool {
619         dirs, err := ioutil.ReadDir(*root)
620         if err != nil {
621                 log.Fatal(err)
622         }
623         hasher := sha256.New()
624         digest := make([]byte, sha256.Size)
625         isGood := true
626         var data []byte
627         var pkgName string
628         for _, dir := range dirs {
629                 files, err := ioutil.ReadDir(filepath.Join(*root, dir.Name()))
630                 if err != nil {
631                         log.Fatal(err)
632                 }
633                 for _, file := range files {
634                         if !strings.HasSuffix(file.Name(), ".sha256") {
635                                 continue
636                         }
637                         pkgName = strings.TrimSuffix(file.Name(), ".sha256")
638                         data, err = ioutil.ReadFile(filepath.Join(*root, dir.Name(), pkgName))
639                         if err != nil {
640                                 if os.IsNotExist(err) {
641                                         continue
642                                 }
643                                 log.Fatal(err)
644                         }
645                         hasher.Write(data)
646                         data, err = ioutil.ReadFile(filepath.Join(*root, dir.Name(), file.Name()))
647                         if err != nil {
648                                 log.Fatal(err)
649                         }
650                         if bytes.Compare(hasher.Sum(digest[:0]), data) == 0 {
651                                 fmt.Println(pkgName, "GOOD")
652                         } else {
653                                 isGood = false
654                                 fmt.Println(pkgName, "BAD")
655                         }
656                         hasher.Reset()
657                 }
658         }
659         return isGood
660 }
661
662 func main() {
663         flag.Parse()
664         if *warranty {
665                 fmt.Println(Warranty)
666                 return
667         }
668         if *version {
669                 fmt.Println("GoCheese version " + Version + " built with " + runtime.Version())
670                 return
671         }
672         if *fsck {
673                 if !goodIntegrity() {
674                         os.Exit(1)
675                 }
676                 return
677         }
678         if *passwdCheck {
679                 refreshPasswd()
680                 return
681         }
682         if (*tlsCert != "" && *tlsKey == "") || (*tlsCert == "" && *tlsKey != "") {
683                 log.Fatalln("Both -tls-cert and -tls-key are required")
684         }
685         var err error
686         pypiURLParsed, err = url.Parse(*pypiURL)
687         if err != nil {
688                 log.Fatalln(err)
689         }
690         refreshPasswd()
691         log.Println("root:", *root, "bind:", *bind)
692
693         ln, err := net.Listen("tcp", *bind)
694         if err != nil {
695                 log.Fatal(err)
696         }
697         ln = netutil.LimitListener(ln, *maxClients)
698         server := &http.Server{
699                 ReadTimeout:  time.Minute,
700                 WriteTimeout: time.Minute,
701         }
702         http.HandleFunc(*norefreshURLPath, handler)
703         http.HandleFunc(*refreshURLPath, handler)
704         http.HandleFunc(*gpgUpdateURLPath, handler)
705
706         needsRefreshPasswd := make(chan os.Signal, 0)
707         needsShutdown := make(chan os.Signal, 0)
708         exitErr := make(chan error, 0)
709         signal.Notify(needsRefreshPasswd, syscall.SIGHUP)
710         signal.Notify(needsShutdown, syscall.SIGTERM, syscall.SIGINT)
711         go func() {
712                 for range needsRefreshPasswd {
713                         log.Println("Refreshing passwords")
714                         refreshPasswd()
715                 }
716         }()
717         go func(s *http.Server) {
718                 <-needsShutdown
719                 killed = true
720                 log.Println("Shutting down")
721                 ctx, cancel := context.WithTimeout(context.TODO(), time.Minute)
722                 exitErr <- s.Shutdown(ctx)
723                 cancel()
724         }(server)
725
726         if *tlsCert == "" {
727                 err = server.Serve(ln)
728         } else {
729                 err = server.ServeTLS(ln, *tlsCert, *tlsKey)
730         }
731         if err != http.ErrServerClosed {
732                 log.Fatal(err)
733         }
734         if err := <-exitErr; err != nil {
735                 log.Fatal(err)
736         }
737 }