]> Cypherpunks.ru repositories - gocheese.git/commitdiff
UCSPI-TCP support
authorSergey Matveev <stargrave@stargrave.org>
Thu, 23 Sep 2021 21:06:19 +0000 (00:06 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Thu, 23 Sep 2021 21:07:23 +0000 (00:07 +0300)
doc/index.texi
doc/ucspi.texi [new file with mode: 0644]
main.go
ucspi.go [new file with mode: 0644]

index fe2ad4e39322efb157e2e4c32829f4c1f6b830cf..93ee48ab20c994655dbeb0728e5bf1de4c01869b 100644 (file)
@@ -52,6 +52,7 @@ but nearly all the code was rewritten. It has huge differences:
 @item No package overwriting ability (as PyPI does too)
 @item Graceful HTTP-server shutdown
 @item Atomic packages store on filesystem
+@item @url{https://cr.yp.to/ucspi-tcp.html, UCSPI-TCP} compatible mode
 @end itemize
 
 Also it contains @file{contrib/pyshop2packages.sh} migration script for
@@ -71,6 +72,7 @@ Please send questions, bug reports and patches to @url{gocheese@@cypherpunks.ru}
 * Install::
 * Usage::
 * Password authentication: Passwords.
+* UCSPI-TCP::
 * TLS support: TLS.
 * Storage format: Storage.
 @end menu
@@ -78,6 +80,7 @@ Please send questions, bug reports and patches to @url{gocheese@@cypherpunks.ru}
 @include install.texi
 @include usage.texi
 @include passwords.texi
+@include ucspi.texi
 @include tls.texi
 @include storage.texi
 
diff --git a/doc/ucspi.texi b/doc/ucspi.texi
new file mode 100644 (file)
index 0000000..56fb85f
--- /dev/null
@@ -0,0 +1,30 @@
+@node UCSPI-TCP
+@unnumbered UCSPI-TCP
+
+You can use GoCheese as UCSPI-TCP service. For example running it also
+under @command{daemontools}:
+
+@example
+# mkdir -p /var/service/.gocheese/log
+# cd /var/service/.gocheese
+
+# cat > run <<EOF
+#!/bin/sh -e
+cd /home/gocheese
+umask 077
+[ -e passwd ] || mkfifo passwd
+( cat passwords.txt > passwd ) &
+umask 022
+mkdir -p packages
+exec setuidgid gocheese tcpserver -DRH -l 0 ::0 8080 \
+    gocheese -ucspi -passwd passwd
+EOF
+
+# cat > log/run <<EOF
+#!/bin/sh -e
+exec setuidgid gocheese multilog t ./main
+EOF
+
+# chmod -R 755 /var/service/.gocheese
+# mv /var/service/.gocheese /var/service/gocheese
+@end example
diff --git a/main.go b/main.go
index cc30de23dfc84596f7980a2fef81fc370827521c..12a325a93435af81d330f2ff9e844e31da891e13 100644 (file)
--- a/main.go
+++ b/main.go
@@ -90,22 +90,28 @@ var (
                HashAlgoMD5,
        }
 
-       root             = flag.String("root", "./packages", "Path to packages directory")
-       bind             = flag.String("bind", "[::]:8080", "Address to bind to")
-       tlsCert          = flag.String("tls-cert", "", "Path to TLS X.509 certificate")
-       tlsKey           = flag.String("tls-key", "", "Path to TLS X.509 private key")
+       root       = flag.String("root", "./packages", "Path to packages directory")
+       bind       = flag.String("bind", "[::]:8080", "Address to bind to")
+       maxClients = flag.Int("maxclients", 128, "Maximal amount of simultaneous clients")
+       doUCSPI    = flag.Bool("ucspi", false, "Work as UCSPI-TCP service")
+
+       tlsCert = flag.String("tls-cert", "", "Path to TLS X.509 certificate")
+       tlsKey  = flag.String("tls-key", "", "Path to TLS X.509 private key")
+
        norefreshURLPath = flag.String("norefresh", "/norefresh/", "Non-refreshing URL path")
        refreshURLPath   = flag.String("refresh", "/simple/", "Auto-refreshing URL path")
        gpgUpdateURLPath = flag.String("gpgupdate", "/gpgupdate/", "GPG forceful refreshing URL path")
-       pypiURL          = flag.String("pypi", "https://pypi.org/simple/", "Upstream (PyPI) URL")
-       pypiCertHash     = flag.String("pypi-cert-hash", "", "Authenticate upstream by its X.509 certificate's SPKI SHA256 hash")
-       logTimestamped   = flag.Bool("log-timestamped", false, "Prepend timestmap to log messages")
-       passwdPath       = flag.String("passwd", "", "Path to FIFO for upload authentication")
-       passwdCheck      = flag.Bool("passwd-check", false, "Run password checker")
-       fsck             = flag.Bool("fsck", false, "Check integrity of all packages (errors are in stderr)")
-       maxClients       = flag.Int("maxclients", 128, "Maximal amount of simultaneous clients")
-       version          = flag.Bool("version", false, "Print version information")
-       warranty         = flag.Bool("warranty", false, "Print warranty information")
+
+       pypiURL      = flag.String("pypi", "https://pypi.org/simple/", "Upstream (PyPI) URL")
+       pypiCertHash = flag.String("pypi-cert-hash", "", "Authenticate upstream by its X.509 certificate's SPKI SHA256 hash")
+
+       passwdPath  = flag.String("passwd", "", "Path to FIFO for upload authentication")
+       passwdCheck = flag.Bool("passwd-check", false, "Run password checker")
+
+       logTimestamped = flag.Bool("log-timestamped", false, "Prepend timestmap to log messages")
+       fsck           = flag.Bool("fsck", false, "Check integrity of all packages (errors are in stderr)")
+       version        = flag.Bool("version", false, "Print version information")
+       warranty       = flag.Bool("warranty", false, "Print warranty information")
 
        killed        bool
        pypiURLParsed *url.URL
@@ -278,7 +284,9 @@ func main() {
        } else {
                log.SetFlags(log.Lshortfile)
        }
-       log.SetOutput(os.Stdout)
+       if !*doUCSPI {
+               log.SetOutput(os.Stdout)
+       }
 
        if *fsck {
                if !goodIntegrity() {
@@ -340,11 +348,6 @@ func main() {
                }
        }
 
-       ln, err := net.Listen("tcp", *bind)
-       if err != nil {
-               log.Fatal(err)
-       }
-       ln = netutil.LimitListener(ln, *maxClients)
        server := &http.Server{
                ReadTimeout:  time.Minute,
                WriteTimeout: time.Minute,
@@ -355,6 +358,24 @@ func main() {
                http.HandleFunc(*gpgUpdateURLPath, handler)
        }
 
+       if *doUCSPI {
+               server.SetKeepAlivesEnabled(false)
+               ln := &UCSPI{}
+               server.ConnState = connStater
+               err := server.Serve(ln)
+               if _, ok := err.(UCSPIAlreadyAccepted); !ok {
+                       log.Fatalln(err)
+               }
+               UCSPIJob.Wait()
+               return
+       }
+
+       ln, err := net.Listen("tcp", *bind)
+       if err != nil {
+               log.Fatal(err)
+       }
+       ln = netutil.LimitListener(ln, *maxClients)
+
        needsShutdown := make(chan os.Signal, 0)
        exitErr := make(chan error, 0)
        signal.Notify(needsShutdown, syscall.SIGTERM, syscall.SIGINT)
diff --git a/ucspi.go b/ucspi.go
new file mode 100644 (file)
index 0000000..8fce33d
--- /dev/null
+++ b/ucspi.go
@@ -0,0 +1,136 @@
+/*
+GoCheese -- Python private package repository and caching proxy
+Copyright (C) 2019-2021 Sergey Matveev <stargrave@stargrave.org>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, version 3 of the License.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+package main
+
+import (
+       "io"
+       "net"
+       "net/http"
+       "os"
+       "sync"
+       "time"
+)
+
+var (
+       aLongTimeAgo = time.Unix(1, 0)
+       UCSPIJob     sync.WaitGroup
+)
+
+type UCSPIAddr struct {
+       ip   string
+       port string
+}
+
+func (addr *UCSPIAddr) Network() string { return "tcp" }
+
+func (addr *UCSPIAddr) String() string { return addr.ip + ":" + addr.port }
+
+type UCSPIConn struct {
+       eof chan struct{}
+}
+
+type ReadResult struct {
+       n   int
+       err error
+}
+
+func (conn *UCSPIConn) Read(b []byte) (int, error) {
+       c := make(chan ReadResult)
+       go func() {
+               n, err := os.Stdin.Read(b)
+               c <- ReadResult{n, err}
+       }()
+       select {
+       case res := <-c:
+               return res.n, res.err
+       case <-conn.eof:
+               return 0, io.EOF
+       }
+}
+
+func (conn *UCSPIConn) Write(b []byte) (int, error) { return os.Stdout.Write(b) }
+
+func (conn *UCSPIConn) Close() error {
+       if err := os.Stdin.Close(); err != nil {
+               return err
+       }
+       return os.Stdout.Close()
+}
+
+func (conn *UCSPIConn) LocalAddr() net.Addr {
+       return &UCSPIAddr{ip: os.Getenv("TCPLOCALIP"), port: os.Getenv("TCPLOCALPORT")}
+}
+
+func (conn *UCSPIConn) RemoteAddr() net.Addr {
+       return &UCSPIAddr{ip: os.Getenv("TCPREMOTEIP"), port: os.Getenv("TCPREMOTEPORT")}
+}
+
+func (conn *UCSPIConn) SetDeadline(t time.Time) error {
+       if err := os.Stdin.SetReadDeadline(t); err != nil {
+               return err
+       }
+       return os.Stdout.SetWriteDeadline(t)
+}
+
+func (conn *UCSPIConn) SetReadDeadline(t time.Time) error {
+       // An ugly hack to forcefully terminate pending read.
+       // net/http calls SetReadDeadline(aLongTimeAgo), but file
+       // descriptors are not capable to exit immediately that way.
+       if t.Equal(aLongTimeAgo) {
+               conn.eof <- struct{}{}
+       }
+       return os.Stdin.SetReadDeadline(t)
+}
+
+func (conn *UCSPIConn) SetWriteDeadline(t time.Time) error {
+       return os.Stdout.SetWriteDeadline(t)
+}
+
+type UCSPI struct{ accepted bool }
+
+type UCSPIAlreadyAccepted struct{}
+
+func (u UCSPIAlreadyAccepted) Error() string {
+       return "already accepted"
+}
+
+func (ucspi *UCSPI) Accept() (net.Conn, error) {
+       if ucspi.accepted {
+               return nil, UCSPIAlreadyAccepted{}
+       }
+       ucspi.accepted = true
+       conn := UCSPIConn{eof: make(chan struct{}, 1)}
+       return &conn, nil
+}
+
+func (ucspi *UCSPI) Close() error {
+       return nil
+}
+
+func (ucspi *UCSPI) Addr() net.Addr {
+       return &UCSPIAddr{ip: os.Getenv("TCPLOCALIP"), port: os.Getenv("TCPLOCALPORT")}
+}
+
+func connStater(conn net.Conn, connState http.ConnState) {
+       switch connState {
+       case http.StateNew:
+               UCSPIJob.Add(1)
+       case http.StateClosed:
+               UCSPIJob.Done()
+       }
+}