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:
87
CLAUDE.md
Normal file
87
CLAUDE.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Gemserve is a simple Gemini protocol server written in Go that serves static files over TLS-encrypted connections. The Gemini protocol is a lightweight, privacy-focused alternative to HTTP designed for serving text-based content.
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build, test, and format everything
|
||||||
|
make
|
||||||
|
|
||||||
|
# Run tests only
|
||||||
|
make test
|
||||||
|
|
||||||
|
# Build binary to ./dist/gemserve
|
||||||
|
make build
|
||||||
|
|
||||||
|
# Format code with gofumpt and gci
|
||||||
|
make fmt
|
||||||
|
|
||||||
|
# Run golangci-lint
|
||||||
|
make lint
|
||||||
|
|
||||||
|
# Run linter with auto-fix
|
||||||
|
make lintfix
|
||||||
|
|
||||||
|
# Clean build artifacts
|
||||||
|
make clean
|
||||||
|
|
||||||
|
# Run the server (after building)
|
||||||
|
./dist/gemserve
|
||||||
|
|
||||||
|
# Generate TLS certificates for development
|
||||||
|
certs/generate.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
- **main.go**: Entry point with TLS server setup, signal handling, and graceful shutdown
|
||||||
|
- **server/**: Request processing, file serving, and Gemini protocol response handling
|
||||||
|
- **gemini/**: Gemini protocol implementation (URL parsing, status codes, path normalization)
|
||||||
|
- **config/**: CLI-based configuration system
|
||||||
|
- **uid/**: Connection ID generation for logging
|
||||||
|
|
||||||
|
### Key Patterns
|
||||||
|
|
||||||
|
- **Security First**: All file operations use `filepath.IsLocal()` and path cleaning to prevent directory traversal
|
||||||
|
- **Error Handling**: Uses structured errors with `xerrors` package for consistent error propagation
|
||||||
|
- **Logging**: Structured logging with configurable levels via internal logging package
|
||||||
|
- **Testing**: Table-driven tests with parallel execution, heavy focus on security edge cases
|
||||||
|
|
||||||
|
### Request Flow
|
||||||
|
|
||||||
|
1. TLS connection established on port 1965
|
||||||
|
2. Read up to 1KB request (Gemini spec limit)
|
||||||
|
3. Parse and normalize Gemini URL
|
||||||
|
4. Validate path security (prevent traversal)
|
||||||
|
5. Serve file or directory index with appropriate MIME type
|
||||||
|
6. Send response with proper Gemini status codes
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Server configured via CLI flags:
|
||||||
|
- `--listen`: Server address (default: localhost:1965)
|
||||||
|
- `--root-path`: Directory to serve files from
|
||||||
|
- `--dir-indexing`: Enable directory browsing
|
||||||
|
- `--log-level`: Logging verbosity
|
||||||
|
- `--response-timeout`: Response timeout in seconds
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
- **server/server_test.go**: Path security and file serving tests
|
||||||
|
- **gemini/url_test.go**: URL parsing and normalization tests
|
||||||
|
- Focus on security edge cases (Unicode, traversal attempts, malformed URLs)
|
||||||
|
- Use parallel test execution for performance
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- All connections require TLS certificates (stored in certs/)
|
||||||
|
- Path traversal protection is critical - test thoroughly when modifying file serving logic
|
||||||
|
- Request size limited to 1KB per Gemini specification
|
||||||
|
- Input validation on all URLs and paths
|
||||||
2
Makefile
2
Makefile
@@ -1,4 +1,4 @@
|
|||||||
SHELL := /bin/env oksh
|
SHELL := /bin/sh
|
||||||
export PATH := $(PATH)
|
export PATH := $(PATH)
|
||||||
|
|
||||||
all: fmt lintfix tidy test clean build
|
all: fmt lintfix tidy test clean build
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ type Config struct {
|
|||||||
ResponseTimeout int // Timeout for responses in seconds
|
ResponseTimeout int // Timeout for responses in seconds
|
||||||
RootPath string // Path to serve files from
|
RootPath string // Path to serve files from
|
||||||
DirIndexingEnabled bool // Allow client to browse directories or not
|
DirIndexingEnabled bool // Allow client to browse directories or not
|
||||||
Listen string // Address to listen on
|
ListenAddr string // Address to listen on
|
||||||
}
|
}
|
||||||
|
|
||||||
var CONFIG Config //nolint:gochecknoglobals
|
var CONFIG Config //nolint:gochecknoglobals
|
||||||
@@ -64,6 +64,6 @@ func GetConfig() *Config {
|
|||||||
ResponseTimeout: *responseTimeout,
|
ResponseTimeout: *responseTimeout,
|
||||||
RootPath: *rootPath,
|
RootPath: *rootPath,
|
||||||
DirIndexingEnabled: *dirIndexing,
|
DirIndexingEnabled: *dirIndexing,
|
||||||
Listen: *listen,
|
ListenAddr: *listen,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
34
gemini/status_codes.go
Normal file
34
gemini/status_codes.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package gemini
|
||||||
|
|
||||||
|
// Gemini status codes as defined in the Gemini spec
|
||||||
|
// gemini://geminiprotocol.net/docs/protocol-specification.gmi
|
||||||
|
const (
|
||||||
|
// Input group
|
||||||
|
StatusInputExpected = 10
|
||||||
|
StatusInputExpectedSensitive = 11
|
||||||
|
|
||||||
|
StatusSuccess = 20
|
||||||
|
|
||||||
|
// Redirect group
|
||||||
|
StatusRedirectTemporary = 30
|
||||||
|
StatusRedirectPermanent = 31
|
||||||
|
|
||||||
|
// Temporary failure group
|
||||||
|
StatusTemporaryFailure = 40
|
||||||
|
StatusServerUnavailable = 41
|
||||||
|
StatusCGIError = 42
|
||||||
|
StatusProxyError = 43
|
||||||
|
StatusSlowDown = 44
|
||||||
|
|
||||||
|
// Permanent failure group
|
||||||
|
StatusPermanentFailure = 50
|
||||||
|
StatusNotFound = 51
|
||||||
|
StatusGone = 52
|
||||||
|
StatusProxyRequestRefused = 53
|
||||||
|
StatusBadRequest = 59
|
||||||
|
|
||||||
|
// TLS certificate group
|
||||||
|
StatusCertificateRequired = 60
|
||||||
|
StatusCertificateNotAuthorized = 61
|
||||||
|
StatusCertificateNotValid = 62
|
||||||
|
)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package common
|
package gemini
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql/driver"
|
"database/sql/driver"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package common
|
package gemini
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/url"
|
"net/url"
|
||||||
10
go.mod
10
go.mod
@@ -2,12 +2,20 @@ module gemserve
|
|||||||
|
|
||||||
go 1.24.3
|
go 1.24.3
|
||||||
|
|
||||||
|
toolchain go1.24.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.antanst.com/antanst/logging v0.0.1
|
git.antanst.com/antanst/logging v0.0.1
|
||||||
git.antanst.com/antanst/xerrors v0.0.1
|
|
||||||
git.antanst.com/antanst/uid v0.0.1
|
git.antanst.com/antanst/uid v0.0.1
|
||||||
|
git.antanst.com/antanst/xerrors v0.0.1
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8
|
github.com/gabriel-vasile/mimetype v1.4.8
|
||||||
github.com/matoous/go-nanoid/v2 v2.1.0
|
github.com/matoous/go-nanoid/v2 v2.1.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require golang.org/x/net v0.33.0 // indirect
|
require golang.org/x/net v0.33.0 // indirect
|
||||||
|
|
||||||
|
replace git.antanst.com/antanst/xerrors => ../xerrors
|
||||||
|
|
||||||
|
replace git.antanst.com/antanst/uid => ../uid
|
||||||
|
|
||||||
|
replace git.antanst.com/antanst/logging => ../logging
|
||||||
|
|||||||
18
main.go
18
main.go
@@ -14,12 +14,16 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gemserve/config"
|
"gemserve/config"
|
||||||
|
"gemserve/gemini"
|
||||||
"gemserve/server"
|
"gemserve/server"
|
||||||
logging "git.antanst.com/antanst/logging"
|
logging "git.antanst.com/antanst/logging"
|
||||||
"git.antanst.com/antanst/uid"
|
"git.antanst.com/antanst/uid"
|
||||||
"git.antanst.com/antanst/xerrors"
|
"git.antanst.com/antanst/xerrors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// This channel is for handling fatal errors
|
||||||
|
// from anywhere in the app. The consumer
|
||||||
|
// should log and panic.
|
||||||
var fatalErrors chan error
|
var fatalErrors chan error
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -37,7 +41,7 @@ func main() {
|
|||||||
func runApp() error {
|
func runApp() error {
|
||||||
logging.LogInfo("Starting up. Press Ctrl+C to exit")
|
logging.LogInfo("Starting up. Press Ctrl+C to exit")
|
||||||
|
|
||||||
listenHost := config.CONFIG.Listen
|
listenAddr := config.CONFIG.ListenAddr
|
||||||
|
|
||||||
signals := make(chan os.Signal, 1)
|
signals := make(chan os.Signal, 1)
|
||||||
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
|
||||||
@@ -45,7 +49,7 @@ func runApp() error {
|
|||||||
fatalErrors = make(chan error)
|
fatalErrors = make(chan error)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
err := startServer(listenHost)
|
err := startServer(listenAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fatalErrors <- xerrors.NewError(err, 0, "Server startup failed", true)
|
fatalErrors <- xerrors.NewError(err, 0, "Server startup failed", true)
|
||||||
}
|
}
|
||||||
@@ -62,7 +66,7 @@ func runApp() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func startServer(listenHost string) (err error) {
|
func startServer(listenAddr string) (err error) {
|
||||||
cert, err := tls.LoadX509KeyPair("certs/server.crt", "certs/server.key")
|
cert, err := tls.LoadX509KeyPair("certs/server.crt", "certs/server.key")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.NewError(fmt.Errorf("failed to load certificate: %w", err), 0, "Certificate loading failed", true)
|
return xerrors.NewError(fmt.Errorf("failed to load certificate: %w", err), 0, "Certificate loading failed", true)
|
||||||
@@ -73,7 +77,7 @@ func startServer(listenHost string) (err error) {
|
|||||||
MinVersion: tls.VersionTLS12,
|
MinVersion: tls.VersionTLS12,
|
||||||
}
|
}
|
||||||
|
|
||||||
listener, err := tls.Listen("tcp", listenHost, tlsConfig)
|
listener, err := tls.Listen("tcp", listenAddr, tlsConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.NewError(fmt.Errorf("failed to create listener: %w", err), 0, "Listener creation failed", true)
|
return xerrors.NewError(fmt.Errorf("failed to create listener: %w", err), 0, "Listener creation failed", true)
|
||||||
}
|
}
|
||||||
@@ -87,7 +91,7 @@ func startServer(listenHost string) (err error) {
|
|||||||
}
|
}
|
||||||
}(listener)
|
}(listener)
|
||||||
|
|
||||||
logging.LogInfo("Server listening on %s", listenHost)
|
logging.LogInfo("Server listening on %s", listenAddr)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
conn, err := listener.Accept()
|
conn, err := listener.Accept()
|
||||||
@@ -106,7 +110,7 @@ func startServer(listenHost string) (err error) {
|
|||||||
fatalErrors <- asErr
|
fatalErrors <- asErr
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
logging.LogWarn("%s %s Connection failed: %d %s (%v)", connId, remoteAddr, asErr.Code, asErr.UserMsg, err)
|
logging.LogWarn("%s %s Connection failed: %v", connId, remoteAddr, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -157,7 +161,7 @@ func handleConnection(conn *tls.Conn, connId string, remoteAddr string) (err err
|
|||||||
code = xErr.Code
|
code = xErr.Code
|
||||||
responseMsg = xErr.UserMsg
|
responseMsg = xErr.UserMsg
|
||||||
} else {
|
} else {
|
||||||
code = 50
|
code = gemini.StatusPermanentFailure
|
||||||
responseMsg = "server error"
|
responseMsg = "server error"
|
||||||
}
|
}
|
||||||
responseHeader = fmt.Sprintf("%d %s", code, responseMsg)
|
responseHeader = fmt.Sprintf("%d %s", code, responseMsg)
|
||||||
|
|||||||
@@ -10,10 +10,11 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"gemserve/common"
|
|
||||||
"gemserve/config"
|
"gemserve/config"
|
||||||
logging "git.antanst.com/antanst/logging"
|
"gemserve/gemini"
|
||||||
|
"git.antanst.com/antanst/logging"
|
||||||
"git.antanst.com/antanst/xerrors"
|
"git.antanst.com/antanst/xerrors"
|
||||||
"github.com/gabriel-vasile/mimetype"
|
"github.com/gabriel-vasile/mimetype"
|
||||||
)
|
)
|
||||||
@@ -23,21 +24,25 @@ type ServerConfig interface {
|
|||||||
RootPath() string
|
RootPath() string
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkRequestURL(url *common.URL) error {
|
func checkRequestURL(url *gemini.URL) error {
|
||||||
if url.Protocol != "gemini" {
|
if !utf8.ValidString(url.String()) {
|
||||||
return xerrors.NewError(fmt.Errorf("invalid URL"), 53, "URL Protocol not Gemini, proxying refused", false)
|
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 {
|
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)
|
listenPort, err := strconv.Atoi(portStr)
|
||||||
if err != nil {
|
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 {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
@@ -45,9 +50,9 @@ func checkRequestURL(url *common.URL) error {
|
|||||||
func GenerateResponse(conn *tls.Conn, connId string, input string) ([]byte, error) {
|
func GenerateResponse(conn *tls.Conn, connId string, input string) ([]byte, error) {
|
||||||
trimmedInput := strings.TrimSpace(input)
|
trimmedInput := strings.TrimSpace(input)
|
||||||
// url will have a cleaned and normalized path after this
|
// 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 {
|
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)
|
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
|
serverRootPath := config.CONFIG.RootPath
|
||||||
localPath, err := calculateLocalPath(url.Path, serverRootPath)
|
localPath, err := calculateLocalPath(url.Path, serverRootPath)
|
||||||
if err != nil {
|
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)
|
logging.LogDebug("%s %s request file path: %s", connId, conn.RemoteAddr(), localPath)
|
||||||
|
|
||||||
// Get file/directory information
|
// Get file/directory information
|
||||||
info, err := os.Stat(localPath)
|
info, err := os.Stat(localPath)
|
||||||
if errors.Is(err, os.ErrNotExist) || errors.Is(err, os.ErrPermission) {
|
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 {
|
} 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.
|
// Handle directory.
|
||||||
@@ -78,12 +83,12 @@ func GenerateResponse(conn *tls.Conn, connId string, input string) ([]byte, erro
|
|||||||
return generateResponseFile(conn, connId, url, localPath)
|
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)
|
data, err := os.ReadFile(localPath)
|
||||||
if errors.Is(err, os.ErrNotExist) || errors.Is(err, os.ErrPermission) {
|
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 {
|
} 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
|
var mimeType string
|
||||||
@@ -92,15 +97,15 @@ func generateResponseFile(conn *tls.Conn, connId string, url *common.URL, localP
|
|||||||
} else {
|
} else {
|
||||||
mimeType = mimetype.Detect(data).String()
|
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...)
|
response := append(headerBytes, data...)
|
||||||
return response, nil
|
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)
|
entries, err := os.ReadDir(localPath)
|
||||||
if err != nil {
|
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 {
|
if config.CONFIG.DirIndexingEnabled {
|
||||||
@@ -115,7 +120,7 @@ func generateResponseDir(conn *tls.Conn, connId string, url *common.URL, localPa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
data := []byte(strings.Join(contents, ""))
|
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...)
|
response := append(headerBytes, data...)
|
||||||
return response, nil
|
return response, nil
|
||||||
} else {
|
} else {
|
||||||
@@ -128,7 +133,7 @@ func generateResponseDir(conn *tls.Conn, connId string, url *common.URL, localPa
|
|||||||
func calculateLocalPath(input string, basePath string) (string, error) {
|
func calculateLocalPath(input string, basePath string) (string, error) {
|
||||||
// Check for invalid characters early
|
// Check for invalid characters early
|
||||||
if strings.ContainsAny(input, "\\") {
|
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)
|
// 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)
|
localPath, err := filepath.Localize(filePath)
|
||||||
if err != nil || !filepath.IsLocal(localPath) {
|
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)
|
filePath = path.Join(basePath, localPath)
|
||||||
|
|||||||
0
srv/index.gmi
Normal file
0
srv/index.gmi
Normal file
Reference in New Issue
Block a user