diff --git a/README.md b/README.md index b9a0837..e5d49c7 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,9 @@ There are few things that can be configured via environment variables: LFS_CONTENTPATH # The path where LFS files are store, default: "lfs-content" LFS_ADMINUSER # An administrator username, default: unset LFS_ADMINPASS # An administrator password, default: unset + LFS_CERT # Certificate file for tls + LFS_KEY # tls key + LFS_SCHEME # set to 'https' to override default http If the `LFS_ADMINUSER` and `LFS_ADMINPASS` variables are set, a rudimentary admin interface can be accessed via @@ -53,7 +56,75 @@ rudimentary admin interface can be accessed via To use the LFS test server with the Git LFS client, configure it in the repository's `.gitconfig` file: + ``` [lfs] url = "http://localhost:8080/janedoe/lfsrepo" + +``` + +HTTPS: + +NOTE: If using https with a self signed cert also disable cert checking in the client repo. + +``` + [lfs] + url = "https://localhost:8080/jimdoe/lfsrepo" + + [http] + selfverify = false + +``` + + +An example usage: + + +Generate a key pair +``` +openssl req -x509 -sha256 -nodes -days 2100 -newkey rsa:2048 -keyout mine.key -out mine.crt ``` + +Make yourself a run script + +``` +#!/bin/bash + +set -eu +set -o pipefail + + +LFS_LISTEN="tcp://:9999" +LFS_HOST="127.0.0.1:9999" +LFS_CONTENTPATH="content" +LFS_ADMINUSER="" +LFS_ADMINPASS="" +LFS_CERT="mine.crt" +LFS_KEY="mine.key" +LFS_SCHEME="https" + +export LFS_LISTEN LFS_HOST LFS_CONTENTPATH LFS_ADMINUSER LFS_ADMINPASS LFS_CERT LFS_KEY LFS_SCHEME + +./lfs-test-server + +``` + +Build the server + +``` +go build + +``` + +Run + +``` +bash run.sh + +``` + +Check the managment page + +browser: https://localhost:9999/mgmt + + diff --git a/config.go b/config.go index e428e88..f87848d 100644 --- a/config.go +++ b/config.go @@ -17,6 +17,13 @@ type Configuration struct { ContentPath string `config:"lfs-content"` AdminUser string `config:""` AdminPass string `config:""` + Cert string `config:""` + Key string `config:""` + Scheme string `config:""` +} + +func (c *Configuration) IsHTTPS() bool { + return strings.Contains(Config.Scheme, "https") } // Config is the global app configuration diff --git a/main.go b/main.go index 2d4f5ca..e836461 100644 --- a/main.go +++ b/main.go @@ -1,11 +1,13 @@ package main import ( + "crypto/tls" "fmt" "net" "os" "os/signal" "syscall" + "time" ) const ( @@ -18,17 +20,68 @@ var ( logger = NewKVLogger(os.Stdout) ) +// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted +// connections. It's used by ListenAndServe and ListenAndServeTLS so +// dead TCP connections (e.g. closing laptop mid-download) eventually +// go away. +type tcpKeepAliveListener struct { + *net.TCPListener +} + +func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { + tc, err := ln.AcceptTCP() + if err != nil { + return + } + tc.SetKeepAlive(true) + tc.SetKeepAlivePeriod(3 * time.Minute) + return tc, nil +} + +func wrapHttps(l net.Listener, cert, key string) (net.Listener, error) { + var err error + + config := &tls.Config{} + + if config.NextProtos == nil { + config.NextProtos = []string{"http/1.1"} + } + + config.Certificates = make([]tls.Certificate, 1) + config.Certificates[0], err = tls.LoadX509KeyPair(cert, key) + if err != nil { + return nil, err + } + + netListener := l.(*TrackingListener).Listener + + tlsListener := tls.NewListener(tcpKeepAliveListener{netListener.(*net.TCPListener)}, config) + return tlsListener, nil +} + func main() { if len(os.Args) == 2 && os.Args[1] == "-v" { fmt.Println(version) os.Exit(0) } + var listener net.Listener + tl, err := NewTrackingListener(Config.Listen) if err != nil { logger.Fatal(kv{"fn": "main", "err": "Could not create listener: " + err.Error()}) } + listener = tl + + if Config.IsHTTPS() { + logger.Log(kv{"fn": "main", "msg": "Using https"}) + listener, err = wrapHttps(tl, Config.Cert, Config.Key) + if err != nil { + logger.Fatal(kv{"fn": "main", "err": "Could not create https listener: " + err.Error()}) + } + } + metaStore, err := NewMetaStore(Config.MetaDB) if err != nil { logger.Fatal(kv{"fn": "main", "err": "Could not open the meta store: " + err.Error()}) @@ -54,6 +107,6 @@ func main() { logger.Log(kv{"fn": "main", "msg": "listening", "pid": os.Getpid(), "addr": Config.Listen, "version": version}) app := NewApp(contentStore, metaStore) - app.Serve(tl) + app.Serve(listener) tl.WaitForChildren() } diff --git a/server.go b/server.go index 7418526..e19bab4 100644 --- a/server.go +++ b/server.go @@ -41,6 +41,11 @@ type Representation struct { // ObjectLink builds a URL linking to the object. func (v *RequestVars) ObjectLink() string { path := fmt.Sprintf("/%s/%s/objects/%s", v.User, v.Repo, v.Oid) + + if Config.IsHTTPS() { + return fmt.Sprintf("%s://%s%s", Config.Scheme, Config.Host, path) + } + return fmt.Sprintf("http://%s%s", Config.Host, path) }