lint and clean up
This commit is contained in:
parent
802bf1eb6d
commit
a53418f77c
5 changed files with 62 additions and 35 deletions
|
@ -1,7 +1,7 @@
|
|||
# gmifs
|
||||
|
||||
Gemini File Server, short gmifs, is intended to be minimal and serve static files. It is used
|
||||
to accompany a hugo blog served via httpd and make it available via the [gemini
|
||||
to accompany a hugo blog served via httpd and makes it available via the [gemini
|
||||
protocol](https://gemini.circumlunar.space/docs/specification.gmi). Why built yet another gemini
|
||||
server? Because it's educational and that's the spirit of the protocol.
|
||||
|
||||
|
|
|
@ -13,13 +13,16 @@ import (
|
|||
"github.com/n0x1m/gmifs/gemini"
|
||||
)
|
||||
|
||||
var errDirWithoutIndexFile = errors.New("path without index.gmi not allowed")
|
||||
var (
|
||||
ErrDirWithoutIndexFile = errors.New("path without index.gmi not allowed")
|
||||
ErrUnsupportedFileType = errors.New("disabled/unsupported file type")
|
||||
)
|
||||
|
||||
func Serve(root string, autoindex bool) func(w io.Writer, r *gemini.Request) {
|
||||
return func(w io.Writer, r *gemini.Request) {
|
||||
fullpath, err := fullPath(root, r.URL.Path)
|
||||
if err != nil {
|
||||
if err == errDirWithoutIndexFile && autoindex {
|
||||
if errors.Is(err, ErrDirWithoutIndexFile) && autoindex {
|
||||
body, mimeType, err := listDirectory(fullpath, r.URL.Path)
|
||||
if err != nil {
|
||||
gemini.WriteHeader(w, gemini.StatusNotFound, err.Error())
|
||||
|
@ -30,9 +33,11 @@ func Serve(root string, autoindex bool) func(w io.Writer, r *gemini.Request) {
|
|||
gemini.Write(w, body)
|
||||
return
|
||||
}
|
||||
|
||||
gemini.WriteHeader(w, gemini.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
body, mimeType, err := readFile(fullpath)
|
||||
if err != nil {
|
||||
gemini.WriteHeader(w, gemini.StatusNotFound, err.Error())
|
||||
|
@ -55,7 +60,7 @@ func fullPath(root, requestPath string) (string, error) {
|
|||
if pathInfo.IsDir() {
|
||||
subDirIndex := path.Join(fullpath, gemini.IndexFile)
|
||||
if _, err := os.Stat(subDirIndex); os.IsNotExist(err) {
|
||||
return fullpath, errDirWithoutIndexFile
|
||||
return fullpath, ErrDirWithoutIndexFile
|
||||
}
|
||||
|
||||
fullpath = subDirIndex
|
||||
|
@ -67,7 +72,7 @@ func fullPath(root, requestPath string) (string, error) {
|
|||
func readFile(filepath string) ([]byte, string, error) {
|
||||
mimeType := getMimeType(filepath)
|
||||
if mimeType == "" {
|
||||
return nil, "", errors.New("disabled/unsupported file type")
|
||||
return nil, "", ErrUnsupportedFileType
|
||||
}
|
||||
|
||||
file, err := os.Open(filepath)
|
||||
|
@ -92,7 +97,7 @@ func getMimeType(fullpath string) string {
|
|||
func listDirectory(fullpath, relpath string) ([]byte, string, error) {
|
||||
files, err := ioutil.ReadDir(fullpath)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, "", fmt.Errorf("list directory: %w", err)
|
||||
}
|
||||
|
||||
var out []byte
|
||||
|
|
|
@ -53,6 +53,11 @@ var (
|
|||
ErrHeaderTooLong = errors.New("gemini: header too long")
|
||||
ErrMissingFile = errors.New("gemini: no such file")
|
||||
ErrEmptyRequest = errors.New("gemini: empty request")
|
||||
ErrEmptyRequestURL = errors.New("gemini: empty request URL")
|
||||
ErrInvalidPath = errors.New("gemini: path error")
|
||||
ErrInvalidHost = errors.New("gemini: empty host")
|
||||
ErrInvalidUtf8 = errors.New("empty request URL")
|
||||
ErrUnknownProtocol = fmt.Errorf("unknown protocol scheme")
|
||||
)
|
||||
|
||||
type Request struct {
|
||||
|
@ -107,6 +112,7 @@ func (s *Server) log(v string) {
|
|||
if s.Logger == nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.Logger.Println("gmifs: " + v)
|
||||
}
|
||||
|
||||
|
@ -114,6 +120,7 @@ func (s *Server) logf(format string, v ...interface{}) {
|
|||
if s.Logger == nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.log(fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
|
@ -130,9 +137,11 @@ func (s *Server) ListenAndServe() error {
|
|||
|
||||
hup := make(chan os.Signal, 1)
|
||||
signal.Notify(hup, syscall.SIGHUP)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
<-hup
|
||||
|
||||
s.log("reloading certificate")
|
||||
if s.listener != nil {
|
||||
err := s.loadTLS()
|
||||
|
@ -150,6 +159,7 @@ func (s *Server) ListenAndServe() error {
|
|||
// TLSConfig.
|
||||
for {
|
||||
s.closed = make(chan struct{})
|
||||
|
||||
var err error
|
||||
s.listener, err = tls.Listen("tcp", s.Addr, s.TLSConfig)
|
||||
if err != nil {
|
||||
|
@ -164,6 +174,7 @@ func (s *Server) ListenAndServe() error {
|
|||
conn, err := s.listener.Accept()
|
||||
if err != nil {
|
||||
s.logf("server accept error: %v", err)
|
||||
|
||||
break
|
||||
}
|
||||
queue <- conn
|
||||
|
@ -173,12 +184,14 @@ func (s *Server) ListenAndServe() error {
|
|||
break
|
||||
}
|
||||
}
|
||||
|
||||
// closed confirms the accept call stopped
|
||||
close(s.closed)
|
||||
if s.shutdown {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
s.log("closing listener gracefully")
|
||||
return s.listener.Close()
|
||||
}
|
||||
|
@ -207,6 +220,7 @@ func (s *Server) handleConnection(conn net.Conn, sem chan struct{}) {
|
|||
case header := <-reqChan:
|
||||
if header.err != nil {
|
||||
s.handleRequestError(conn, header)
|
||||
|
||||
return
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
@ -234,6 +248,7 @@ func (s *Server) handleRequestError(conn net.Conn, req request) {
|
|||
var gmierr *GmiError
|
||||
if errors.As(req.err, &gmierr) {
|
||||
WriteHeader(conn, gmierr.Code, gmierr.Error())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -270,11 +285,11 @@ func readHeader(c net.Conn) (*request, error) {
|
|||
|
||||
requestURL := strings.TrimSpace(req)
|
||||
if requestURL == "" {
|
||||
return r, Error(StatusBadRequest, errors.New("empty request URL"))
|
||||
} else if !utf8.ValidString(requestURL) {
|
||||
return r, Error(StatusBadRequest, errors.New("not a valid utf-8 url"))
|
||||
return r, Error(StatusBadRequest, ErrEmptyRequestURL)
|
||||
} else if len(requestURL) > URLMaxBytes {
|
||||
return r, Error(StatusBadRequest, ErrHeaderTooLong)
|
||||
} else if !utf8.ValidString(requestURL) {
|
||||
return r, Error(StatusBadRequest, ErrInvalidUtf8)
|
||||
}
|
||||
|
||||
parsedURL, err := url.Parse(requestURL)
|
||||
|
@ -285,15 +300,16 @@ func readHeader(c net.Conn) (*request, error) {
|
|||
r.URL = parsedURL
|
||||
|
||||
if parsedURL.Scheme != "" && parsedURL.Scheme != "gemini" {
|
||||
return r, Error(StatusProxyRequestRefused, fmt.Errorf("unknown protocol scheme %s", parsedURL.Scheme))
|
||||
return r, Error(StatusProxyRequestRefused, ErrUnknownProtocol)
|
||||
} else if parsedURL.Host == "" {
|
||||
return r, Error(StatusBadRequest, errors.New("empty host"))
|
||||
return r, Error(StatusBadRequest, ErrInvalidHost)
|
||||
}
|
||||
|
||||
if parsedURL.Path == "" {
|
||||
// This error is a redirect path.
|
||||
return r, Error(StatusRedirectPermanent, errors.New("./"+parsedURL.Path))
|
||||
} else if parsedURL.Path != path.Clean(parsedURL.Path) {
|
||||
return r, Error(StatusBadRequest, errors.New("path error"))
|
||||
return r, Error(StatusBadRequest, ErrInvalidPath)
|
||||
}
|
||||
|
||||
return r, nil
|
||||
|
@ -308,11 +324,10 @@ func (s *Server) Shutdown(ctx context.Context) error {
|
|||
go func() {
|
||||
s.shutdown = true
|
||||
// un-stuck call to self
|
||||
conn, err := tls.Dial("tcp", "localhost:1965", &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
})
|
||||
conn, err := tls.Dial("tcp", s.Addr, &tls.Config{InsecureSkipVerify: true})
|
||||
if err != nil {
|
||||
s.logf("un-stuck call error: %v", err)
|
||||
s.logf("un-stuck call failed (ok): %v", err)
|
||||
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
@ -327,20 +342,21 @@ func (s *Server) Shutdown(ctx context.Context) error {
|
|||
s.logf("error while closing listener %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func WriteHeader(c io.Writer, code int, message string) {
|
||||
func WriteHeader(c io.Writer, code int, message string) (int, error) {
|
||||
// <STATUS><SPACE><META><CR><LF>
|
||||
var header []byte
|
||||
if len(message) == 0 {
|
||||
header = []byte(fmt.Sprintf("%d%s", code, Termination))
|
||||
}
|
||||
header = []byte(fmt.Sprintf("%d %s%s", code, message, Termination))
|
||||
c.Write(header)
|
||||
return c.Write([]byte(fmt.Sprintf("%d%s", code, Termination)))
|
||||
}
|
||||
|
||||
func Write(c io.Writer, body []byte) {
|
||||
reader := bytes.NewReader(body)
|
||||
io.Copy(c, reader)
|
||||
return c.Write([]byte(fmt.Sprintf("%d %s%s", code, message, Termination)))
|
||||
}
|
||||
|
||||
func Write(c io.Writer, body []byte) (int64, error) {
|
||||
reader := bytes.NewReader(body)
|
||||
|
||||
return io.Copy(c, reader)
|
||||
}
|
||||
|
|
|
@ -6,10 +6,13 @@ import (
|
|||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"time"
|
||||
)
|
||||
|
||||
const rsaBits = 2048
|
||||
|
||||
// GenX509KeyPair generates a TLS keypair with one week validity.
|
||||
func GenX509KeyPair(host string, daysvalid int) (tls.Certificate, error) {
|
||||
now := time.Now()
|
||||
|
@ -28,15 +31,15 @@ func GenX509KeyPair(host string, daysvalid int) (tls.Certificate, error) {
|
|||
x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||
}
|
||||
|
||||
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
priv, err := rsa.GenerateKey(rand.Reader, rsaBits)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, err
|
||||
return tls.Certificate{}, fmt.Errorf("generate key: %w", err)
|
||||
}
|
||||
|
||||
cert, err := x509.CreateCertificate(rand.Reader, template, template,
|
||||
priv.Public(), priv)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, err
|
||||
return tls.Certificate{}, fmt.Errorf("create certificate: %w", err)
|
||||
}
|
||||
|
||||
var out tls.Certificate
|
||||
|
|
13
main.go
13
main.go
|
@ -84,10 +84,12 @@ func main() {
|
|||
}
|
||||
|
||||
confirm := make(chan struct{}, 1)
|
||||
|
||||
go func() {
|
||||
if err := server.ListenAndServe(); err != nil && !errors.Is(err, gemini.ErrServerClosed) {
|
||||
log.Fatalf("ListenAndServe terminated unexpectedly: %v", err)
|
||||
}
|
||||
|
||||
close(confirm)
|
||||
}()
|
||||
|
||||
|
@ -110,7 +112,7 @@ func setupCertificate(crt, key, host string) func() (*tls.Config, error) {
|
|||
if crt != "" && key != "" {
|
||||
cert, err := tls.LoadX509KeyPair(crt, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("load x509 keypair: %w", err)
|
||||
}
|
||||
return gemini.TLSConfig(host, cert), nil
|
||||
}
|
||||
|
@ -118,7 +120,7 @@ func setupCertificate(crt, key, host string) func() (*tls.Config, error) {
|
|||
log.Println("generating self-signed temporary certificate")
|
||||
cert, err := gemini.GenX509KeyPair(host, autoCertDaysValid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("generate x509 keypair: %w", err)
|
||||
}
|
||||
return gemini.TLSConfig(host, cert), nil
|
||||
}
|
||||
|
@ -132,12 +134,13 @@ func setupLogger(dir, filename string) (*log.Logger, error) {
|
|||
logpath := filepath.Join(dir, filename)
|
||||
_, err := setupFileLogging(logger, logpath)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to open log file: %v", err)
|
||||
return logger, fmt.Errorf("setup logger: %w", err)
|
||||
}
|
||||
|
||||
go func(logger *log.Logger, logpath string) {
|
||||
hup := make(chan os.Signal, 1)
|
||||
signal.Notify(hup, syscall.SIGHUP)
|
||||
|
||||
for {
|
||||
<-hup
|
||||
logger.Println("rotating log file after SIGHUP")
|
||||
|
@ -146,16 +149,16 @@ func setupLogger(dir, filename string) (*log.Logger, error) {
|
|||
log.Fatalf("failed to rotate log file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
}(logger, logpath)
|
||||
}
|
||||
|
||||
return logger, nil
|
||||
}
|
||||
|
||||
func setupFileLogging(logger *log.Logger, logpath string) (*os.File, error) {
|
||||
logfile, err := os.OpenFile(logpath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
return logfile, err
|
||||
return logfile, fmt.Errorf("log file open: %w", err)
|
||||
}
|
||||
|
||||
logger.SetOutput(logfile)
|
||||
|
|
Loading…
Reference in a new issue