--- /dev/null
+/*
+godlighty -- highly-customizable HTTP, HTTP/2, HTTPS server
+Copyright (C) 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 godlighty
+
+import (
+ "crypto/sha256"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "strings"
+)
+
+var Unauthorized = errors.New("failed authorization")
+
+func performAuth(w http.ResponseWriter, r *http.Request, cfg *AuthCfg) (string, error) {
+ username, password, ok := r.BasicAuth()
+ if !ok {
+ w.Header().Add("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, cfg.Realm))
+ w.WriteHeader(http.StatusUnauthorized)
+ return username, Unauthorized
+ }
+ data, err := ioutil.ReadFile(cfg.Passwords)
+ if err != nil {
+ return username, err
+ }
+ for _, line := range strings.Split(string(data), "\n") {
+ if len(line) == 0 || line[0] == '#' {
+ continue
+ }
+ cols := strings.Split(line, ":")
+ if len(cols) != 2 {
+ continue
+ }
+ if cols[0] == username {
+ d := sha256.Sum256([]byte(password))
+ if hex.EncodeToString(d[:]) == cols[1] {
+ return username, nil
+ } else {
+ w.WriteHeader(http.StatusUnauthorized)
+ return username, Unauthorized
+ }
+ }
+ }
+ w.WriteHeader(http.StatusUnauthorized)
+ return username, Unauthorized
+}
Cert string
Key string
CACert string
+
+ // Require client authentication
+ ClientCAs []string
+}
+
+type AuthCfg struct {
+ Passwords string
+ Realm string
}
type Hook func(http.ResponseWriter, *http.Request) bool
DirList bool
WebDAV bool
Hooks []Hook
+ Auth *AuthCfg
Indexes []string
Readmes []string
}()
var ll net.Listener
if *doTLS {
- tlsCfg := tls.Config{
- GetCertificate: godlighty.GetCertificate,
- NextProtos: []string{"h2", "http/1.1"},
- }
- ll = tls.NewListener(netutil.LimitListener(l, MaxConns), &tlsCfg)
+ tlsCfg := godlighty.NewTLSConfig()
+ ll = tls.NewListener(netutil.LimitListener(l, MaxConns), tlsCfg)
} else {
ll = netutil.LimitListener(l, MaxConns)
}
@item Auto-generated directory listings and
read-only @url{https://en.wikipedia.org/wiki/WebDAV, WebDAV} support.
+@item Per-domain HTTP basic authorization and TLS client authentication.
+
@item If corresponding @file{.meta4} files are found, then @code{Link}
header is generated automatically to that
@url{http://www.metalinker.org/, Metalink}.
import (
"bytes"
"compress/gzip"
+ "errors"
"fmt"
"io/ioutil"
"log"
)
var (
- CompressibleContentTypes = make(map[string]struct{})
-
gzPool = sync.Pool{
New: func() interface{} { return gzip.NewWriter(ioutil.Discard) },
}
return
}
- for _, hook := range cfg.Hooks {
- if done := hook(w, r); done {
- return
- }
+ var username string
+ var err error
+ if cfg.Auth != nil {
+ username, err = performAuth(w, r, cfg.Auth)
+ }
+ if username != "" {
+ username = "user:" + username + " "
}
-
printErr := func(code int, err error) {
- fmt.Printf("%s %s \"%s %s %s\" %d \"%s\" \"%s\"\n",
+ fmt.Printf("%s %s \"%s %s %s\" %d \"%s\" %s\"%s\"\n",
r.RemoteAddr, host, r.Method, r.URL.Path, r.Proto,
code, err.Error(),
- r.Header.Get("User-Agent"),
+ username, r.Header.Get("User-Agent"),
)
}
+ switch err {
+ case nil:
+ break
+ case Unauthorized:
+ printErr(http.StatusUnauthorized, err)
+ return
+ default:
+ printErr(http.StatusInternalServerError, err)
+ http.Error(w, "internal error", http.StatusInternalServerError)
+ return
+ }
+
+ if cfg.TLS != nil && len(cfg.TLS.ClientCAs) > 0 {
+ if r.TLS == nil {
+ err = errors.New("TLS client authentication required")
+ printErr(http.StatusForbidden, err)
+ http.Error(w, err.Error(), http.StatusForbidden)
+ return
+ } else {
+ username += r.TLS.PeerCertificates[0].Subject.String() + " "
+ }
+ }
+
+ for _, hook := range cfg.Hooks {
+ if done := hook(w, r); done {
+ return
+ }
+ }
if cfg.Root == "" {
notFound()
}
wc := &CountResponseWriter{ResponseWriter: w}
dav.ServeHTTP(wc, r)
- fmt.Printf("%s %s \"WebDAV %s\" %d %d \"%s\"\n",
+ fmt.Printf("%s %s \"WebDAV %s\" %d %d %s\"%s\"\n",
r.RemoteAddr, host, r.URL.Path,
wc.Status, wc.Size,
- r.Header.Get("User-Agent"),
+ username, r.Header.Get("User-Agent"),
)
return
}
if !(r.Method == "" || r.Method == http.MethodGet) {
- fmt.Printf("%s %s \"%s %s %s\" %d \"%s\"\n",
+ fmt.Printf("%s %s \"%s %s %s\" %d %s\"%s\"\n",
r.RemoteAddr, host, r.Method, r.URL.Path, r.Proto,
http.StatusMethodNotAllowed,
- r.Header.Get("User-Agent"),
+ username, r.Header.Get("User-Agent"),
)
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
wr := wc.(*gzipResponseWriter)
w.WriteHeader(wr.status)
w.Write(bufCompressed.Bytes())
- fmt.Printf("%s %s \"%s %s %s\" %d %d \"%s\"\n",
+ fmt.Printf("%s %s \"%s %s %s\" %d %d %s\"%s\"\n",
r.RemoteAddr, host, r.Method, r.URL.Path, r.Proto,
wr.status, size,
- r.Header.Get("User-Agent"),
+ username, r.Header.Get("User-Agent"),
)
return
}
wr := wc.(*CountResponseWriter)
- fmt.Printf("%s %s \"%s %s %s\" %d %d \"%s\"\n",
+ fmt.Printf("%s %s \"%s %s %s\" %d %d %s\"%s\"\n",
r.RemoteAddr, host, r.Method, r.URL.Path, r.Proto,
wr.Status, wr.Size,
- r.Header.Get("User-Agent"),
+ username, r.Header.Get("User-Agent"),
)
}
const OctetStream = "application/octet-stream"
-var ContentTypes = make(map[string]string)
+var (
+ ContentTypes = make(map[string]string)
+ CompressibleContentTypes = make(map[string]struct{})
+)
func mediaType(fn string, overrides map[string]string) string {
ext := path.Ext(fn)
import (
"crypto/tls"
+ "crypto/x509"
"encoding/pem"
"errors"
"fmt"
"log"
)
-var HostToCertificate map[string]*tls.Certificate
+var (
+ NextProtos = []string{"h2", "http/1.1"}
+ HostToCertificate map[string]*tls.Certificate
+ HostClientAuth map[string]*x509.CertPool
+)
func GetCertificate(chi *tls.ClientHelloInfo) (*tls.Certificate, error) {
cert := HostToCertificate[chi.ServerName]
return cert, nil
}
+func GetConfigForClient(chi *tls.ClientHelloInfo) (*tls.Config, error) {
+ pool := HostClientAuth[chi.ServerName]
+ if pool == nil {
+ return nil, nil
+ }
+ return &tls.Config{
+ GetCertificate: GetCertificate,
+ NextProtos: NextProtos,
+ ClientCAs: pool,
+ ClientAuth: tls.RequireAndVerifyClientCert,
+ }, nil
+}
+
func LoadCertificates() {
HostToCertificate = make(map[string]*tls.Certificate, len(Hosts))
+ HostClientAuth = make(map[string]*x509.CertPool)
for host, cfg := range Hosts {
if cfg.TLS == nil {
continue
cert.Certificate = append(cert.Certificate, block.Bytes)
}
HostToCertificate[host] = &cert
+ pool := x509.NewCertPool()
+ for _, p := range cfg.TLS.ClientCAs {
+ data, err := ioutil.ReadFile(p)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ var block *pem.Block
+ for len(data) > 0 {
+ block, data = pem.Decode(data)
+ if block == nil {
+ log.Fatalln("can not decode PEM:", p)
+ }
+ if block.Type != "CERTIFICATE" {
+ continue
+ }
+ ca, err := x509.ParseCertificate(block.Bytes)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ pool.AddCert(ca)
+ }
+ }
+ if len(pool.Subjects()) > 0 {
+ HostClientAuth[host] = pool
+ }
+ }
+}
+
+func NewTLSConfig() *tls.Config {
+ return &tls.Config{
+ NextProtos: NextProtos,
+ GetCertificate: GetCertificate,
+ GetConfigForClient: GetConfigForClient,
}
}