Refactor Gemini protocol implementation and improve server architecture
- Move gemini URL parsing from common/ to gemini/ package - Add structured status codes in gemini/status_codes.go - Improve error handling with proper Gemini status codes - Update configuration field naming (Listen -> ListenAddr) - Add UTF-8 validation for URLs - Enhance security with better path validation - Add CLAUDE.md for development guidance - Include example content in srv/ directory - Update build system to use standard shell
This commit is contained in:
@@ -10,10 +10,11 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"gemserve/common"
|
||||
"gemserve/config"
|
||||
logging "git.antanst.com/antanst/logging"
|
||||
"gemserve/gemini"
|
||||
"git.antanst.com/antanst/logging"
|
||||
"git.antanst.com/antanst/xerrors"
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
)
|
||||
@@ -23,21 +24,25 @@ type ServerConfig interface {
|
||||
RootPath() string
|
||||
}
|
||||
|
||||
func checkRequestURL(url *common.URL) error {
|
||||
if url.Protocol != "gemini" {
|
||||
return xerrors.NewError(fmt.Errorf("invalid URL"), 53, "URL Protocol not Gemini, proxying refused", false)
|
||||
func checkRequestURL(url *gemini.URL) error {
|
||||
if !utf8.ValidString(url.String()) {
|
||||
return xerrors.NewError(fmt.Errorf("invalid URL"), gemini.StatusBadRequest, "Invalid URL", false)
|
||||
}
|
||||
|
||||
_, portStr, err := net.SplitHostPort(config.CONFIG.Listen)
|
||||
if url.Protocol != "gemini" {
|
||||
return xerrors.NewError(fmt.Errorf("invalid URL"), gemini.StatusProxyRequestRefused, "URL Protocol not Gemini, proxying refused", false)
|
||||
}
|
||||
|
||||
_, portStr, err := net.SplitHostPort(config.CONFIG.ListenAddr)
|
||||
if err != nil {
|
||||
return xerrors.NewError(fmt.Errorf("failed to parse listen address: %w", err), 50, "Server configuration error", false)
|
||||
return xerrors.NewError(fmt.Errorf("failed to parse server listen address: %w", err), 0, "", true)
|
||||
}
|
||||
listenPort, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
return xerrors.NewError(fmt.Errorf("failed to parse listen port: %w", err), 50, "Server configuration error", false)
|
||||
return xerrors.NewError(fmt.Errorf("invalid server listen port: %w", err), 0, "", true)
|
||||
}
|
||||
if url.Port != listenPort {
|
||||
return xerrors.NewError(fmt.Errorf("failed to parse URL: %w", err), 53, "invalid URL port, proxying refused", false)
|
||||
return xerrors.NewError(fmt.Errorf("failed to parse URL: %w", err), gemini.StatusProxyRequestRefused, "invalid request port, proxying refused", false)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -45,9 +50,9 @@ func checkRequestURL(url *common.URL) error {
|
||||
func GenerateResponse(conn *tls.Conn, connId string, input string) ([]byte, error) {
|
||||
trimmedInput := strings.TrimSpace(input)
|
||||
// url will have a cleaned and normalized path after this
|
||||
url, err := common.ParseURL(trimmedInput, "", true)
|
||||
url, err := gemini.ParseURL(trimmedInput, "", true)
|
||||
if err != nil {
|
||||
return nil, xerrors.NewError(fmt.Errorf("failed to parse URL: %w", err), 59, "Invalid URL", false)
|
||||
return nil, xerrors.NewError(fmt.Errorf("failed to parse URL: %w", err), gemini.StatusBadRequest, "Invalid URL", false)
|
||||
}
|
||||
logging.LogDebug("%s %s normalized URL path: %s", connId, conn.RemoteAddr(), url.Path)
|
||||
|
||||
@@ -59,16 +64,16 @@ func GenerateResponse(conn *tls.Conn, connId string, input string) ([]byte, erro
|
||||
serverRootPath := config.CONFIG.RootPath
|
||||
localPath, err := calculateLocalPath(url.Path, serverRootPath)
|
||||
if err != nil {
|
||||
return nil, xerrors.NewError(err, 59, "Invalid path", false)
|
||||
return nil, xerrors.NewError(err, gemini.StatusBadRequest, "Invalid path", false)
|
||||
}
|
||||
logging.LogDebug("%s %s request file path: %s", connId, conn.RemoteAddr(), localPath)
|
||||
|
||||
// Get file/directory information
|
||||
info, err := os.Stat(localPath)
|
||||
if errors.Is(err, os.ErrNotExist) || errors.Is(err, os.ErrPermission) {
|
||||
return []byte("51 not found\r\n"), nil
|
||||
return nil, xerrors.NewError(fmt.Errorf("%s %s failed to access path: %w", connId, conn.RemoteAddr(), err), gemini.StatusNotFound, "Not found or access denied", false)
|
||||
} else if err != nil {
|
||||
return nil, xerrors.NewError(fmt.Errorf("%s %s failed to access path: %w", connId, conn.RemoteAddr(), err), 0, "Path access failed", false)
|
||||
return nil, xerrors.NewError(fmt.Errorf("%s %s failed to access path: %w", connId, conn.RemoteAddr(), err), gemini.StatusNotFound, "Path access failed", false)
|
||||
}
|
||||
|
||||
// Handle directory.
|
||||
@@ -78,12 +83,12 @@ func GenerateResponse(conn *tls.Conn, connId string, input string) ([]byte, erro
|
||||
return generateResponseFile(conn, connId, url, localPath)
|
||||
}
|
||||
|
||||
func generateResponseFile(conn *tls.Conn, connId string, url *common.URL, localPath string) ([]byte, error) {
|
||||
func generateResponseFile(conn *tls.Conn, connId string, url *gemini.URL, localPath string) ([]byte, error) {
|
||||
data, err := os.ReadFile(localPath)
|
||||
if errors.Is(err, os.ErrNotExist) || errors.Is(err, os.ErrPermission) {
|
||||
return []byte("51 not found\r\n"), nil
|
||||
return nil, xerrors.NewError(fmt.Errorf("%s %s failed to access path: %w", connId, conn.RemoteAddr(), err), gemini.StatusNotFound, "Path access failed", false)
|
||||
} else if err != nil {
|
||||
return nil, xerrors.NewError(fmt.Errorf("%s %s failed to read file: %w", connId, conn.RemoteAddr(), err), 0, "File read failed", false)
|
||||
return nil, xerrors.NewError(fmt.Errorf("%s %s failed to read file: %w", connId, conn.RemoteAddr(), err), gemini.StatusNotFound, "Path access failed", false)
|
||||
}
|
||||
|
||||
var mimeType string
|
||||
@@ -92,15 +97,15 @@ func generateResponseFile(conn *tls.Conn, connId string, url *common.URL, localP
|
||||
} else {
|
||||
mimeType = mimetype.Detect(data).String()
|
||||
}
|
||||
headerBytes := []byte(fmt.Sprintf("20 %s; lang=en\r\n", mimeType))
|
||||
headerBytes := []byte(fmt.Sprintf("%d %s; lang=en\r\n", gemini.StatusSuccess, mimeType))
|
||||
response := append(headerBytes, data...)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func generateResponseDir(conn *tls.Conn, connId string, url *common.URL, localPath string) (output []byte, err error) {
|
||||
func generateResponseDir(conn *tls.Conn, connId string, url *gemini.URL, localPath string) (output []byte, err error) {
|
||||
entries, err := os.ReadDir(localPath)
|
||||
if err != nil {
|
||||
return nil, xerrors.NewError(fmt.Errorf("%s %s failed to read directory: %w", connId, conn.RemoteAddr(), err), 0, "Directory read failed", false)
|
||||
return nil, xerrors.NewError(fmt.Errorf("%s %s failed to read directory: %w", connId, conn.RemoteAddr(), err), gemini.StatusNotFound, "Directory access failed", false)
|
||||
}
|
||||
|
||||
if config.CONFIG.DirIndexingEnabled {
|
||||
@@ -115,7 +120,7 @@ func generateResponseDir(conn *tls.Conn, connId string, url *common.URL, localPa
|
||||
}
|
||||
}
|
||||
data := []byte(strings.Join(contents, ""))
|
||||
headerBytes := []byte("20 text/gemini; lang=en\r\n")
|
||||
headerBytes := []byte(fmt.Sprintf("%d text/gemini; lang=en\r\n", gemini.StatusSuccess))
|
||||
response := append(headerBytes, data...)
|
||||
return response, nil
|
||||
} else {
|
||||
@@ -128,7 +133,7 @@ func generateResponseDir(conn *tls.Conn, connId string, url *common.URL, localPa
|
||||
func calculateLocalPath(input string, basePath string) (string, error) {
|
||||
// Check for invalid characters early
|
||||
if strings.ContainsAny(input, "\\") {
|
||||
return "", xerrors.NewError(fmt.Errorf("invalid characters in path: %s", input), 0, "Invalid path characters", false)
|
||||
return "", xerrors.NewError(fmt.Errorf("invalid characters in path: %s", input), gemini.StatusBadRequest, "Invalid path", false)
|
||||
}
|
||||
|
||||
// If IsLocal(path) returns true, then Join(base, path)
|
||||
@@ -144,7 +149,7 @@ func calculateLocalPath(input string, basePath string) (string, error) {
|
||||
|
||||
localPath, err := filepath.Localize(filePath)
|
||||
if err != nil || !filepath.IsLocal(localPath) {
|
||||
return "", xerrors.NewError(fmt.Errorf("could not construct local path from %s: %s", input, err), 0, "Invalid local path", false)
|
||||
return "", xerrors.NewError(fmt.Errorf("could not construct local path from %s: %s", input, err), gemini.StatusBadRequest, "Invalid path", false)
|
||||
}
|
||||
|
||||
filePath = path.Join(basePath, localPath)
|
||||
|
||||
Reference in New Issue
Block a user