aboutsummaryrefslogtreecommitdiff
path: root/handler.go
blob: b913906c7e66d82ab74ebbd702996b9930ee1dad (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
package main

import (
	"bytes"
	"context"
	"fmt"
	"io/ioutil"
	"log"
	"net/url"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

	gemini "git.sr.ht/~yotam/go-gemini"
)

// Handler is the main handler of the server
type Handler struct {
	cfg Config
}

func (h Handler) validateRequest(r gemini.Request, absItemPath string) error {
	u, err := url.Parse(r.URL)
	if err != nil {
		return gemini.Error{Err: err, Status: gemini.StatusBadRequest}
	}

	if u.Scheme != "" && u.Scheme != "gemini" {
		err = fmt.Errorf("proxy is not supported by the server")
		return gemini.Error{Err: err, Status: gemini.StatusProxyRequestRefused}
	}

	if !strings.HasPrefix(absItemPath, h.cfg.SourceDir) {
		return gemini.Error{Err: fmt.Errorf("permission denied"), Status: gemini.StatusBadRequest}
	}

	return nil
}

func (h Handler) urlAbsPath(rawURL string) (string, error) {
	u, err := url.Parse(rawURL)
	if err != nil {
		return "", gemini.Error{Err: err, Status: gemini.StatusBadRequest}
	}

	itemPath, err := filepath.Abs(filepath.Join(h.cfg.SourceDir, u.Path))
	if err != nil {
		return "", gemini.Error{Err: err, Status: gemini.StatusTemporaryFailure}
	}

	return itemPath, nil
}

func (h Handler) getFilePath(rawURL string) (string, error) {
	itemPath, err := h.urlAbsPath(rawURL)
	if err != nil {
		return "", err
	}

	if isFile(itemPath) {
		return itemPath, nil
	}

	for _, indexFile := range h.cfg.IndexFiles {
		indexPath := filepath.Join(itemPath, indexFile)
		if isFile(indexPath) {
			return indexPath, nil
		}
	}

	return "", gemini.Error{Err: fmt.Errorf("file not found"), Status: gemini.StatusNotFound}
}

func (h Handler) serveExecutable(r gemini.Request, path string) gemini.Response {
	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(h.cfg.ExecTimeout)*time.Second)
	defer cancel()

	cmd := exec.CommandContext(ctx, path)

	stdin, err := cmd.StdinPipe()
	if err != nil {
		return gemini.ErrorResponse(err)
	}
	defer stdin.Close()

	_, err = fmt.Fprintf(stdin, "%s\r\n", r.URL)
	if err != nil {
		return gemini.ErrorResponse(err)
	}

	// The gemini library api make it hard to stream stdout instead of reading it into memory
	out, err := cmd.Output()
	if err != nil {
		return gemini.ErrorResponse(err)
	}

	if ctx.Err() == context.DeadlineExceeded {
		return gemini.ErrorResponse(ctx.Err())
	}

	return gemini.Response{Status: 20, Meta: "text/gemini", Body: ioutil.NopCloser(bytes.NewReader(out))}
}

func (h Handler) serveFile(path string) gemini.Response {
	log.Println("Serving file from", path)

	file, err := os.Open(path)
	if err != nil {
		return gemini.ErrorResponse(err)
	}

	meta := absPathMime(path)
	return gemini.Response{Status: gemini.StatusSuccess, Meta: meta, Body: file}
}

// Handle implement the gemini.Handler interface by serving files from a given source directory
func (h Handler) Handle(r gemini.Request) gemini.Response {
	path, err := h.getFilePath(r.URL)
	if err != nil {
		return gemini.ErrorResponse(err)
	}

	err = h.validateRequest(r, path)
	if err != nil {
		return gemini.ErrorResponse(err)
	}

	if h.cfg.ExecuteFiles && isExecutable(path) {
		return h.serveExecutable(r, path)
	}

	return h.serveFile(path)
}