parent
8da7501544
commit
d8c6a7cf20
@ -0,0 +1,12 @@ |
||||
module beastieboy/marmotte |
||||
|
||||
go 1.19 |
||||
|
||||
require ( |
||||
github.com/gabriel-vasile/mimetype v1.4.1 // indirect |
||||
github.com/mattn/go-colorable v0.1.13 // indirect |
||||
github.com/mattn/go-isatty v0.0.16 // indirect |
||||
github.com/rs/zerolog v1.28.0 // indirect |
||||
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect |
||||
golang.org/x/sys v0.0.0-20221010170243-090e33056c14 // indirect |
||||
) |
@ -0,0 +1,26 @@ |
||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= |
||||
github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q= |
||||
github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M= |
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= |
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= |
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= |
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= |
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= |
||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= |
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= |
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= |
||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= |
||||
github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY= |
||||
github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= |
||||
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ= |
||||
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= |
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
||||
golang.org/x/sys v0.0.0-20221010170243-090e33056c14 h1:k5II8e6QD8mITdi+okbbmR/cIyEbeXLBhy5Ha4nevyc= |
||||
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= |
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= |
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= |
@ -0,0 +1,56 @@ |
||||
package gopher |
||||
|
||||
import ( |
||||
"regexp" |
||||
|
||||
"github.com/rs/zerolog/log" |
||||
) |
||||
|
||||
type Context struct { |
||||
Root string |
||||
Host string |
||||
Port string |
||||
TransportProtocol string |
||||
RequestTransformers []RequestTransformer |
||||
ResponseTransformers []ResponseTransformer |
||||
Header []string |
||||
Footer []string |
||||
} |
||||
|
||||
func (c *Context) AddRequestTransformer(t RequestTransformer) ([]RequestTransformer, error) { |
||||
c.RequestTransformers = append(c.RequestTransformers, t) |
||||
return c.RequestTransformers, nil |
||||
} |
||||
|
||||
func (c *Context) AddRequestTransformerFor(s string, f RequestTransformerFunc) ([]RequestTransformer, error) { |
||||
_, e := regexp.Compile(s) |
||||
if e != nil { |
||||
log.Err(e). |
||||
Str("string", s). |
||||
Send() |
||||
return c.RequestTransformers, e |
||||
} else { |
||||
t := RequestTransformer{Transformer: f, |
||||
Predicate: FullPathMatchPredicate, |
||||
} |
||||
return c.AddRequestTransformer(t) |
||||
} |
||||
} |
||||
|
||||
func (c *Context) DefaultResponseTransformers() []ResponseTransformer { |
||||
gophermap := ResponseTransformer{ |
||||
Transformer: GopherMapTransformer, |
||||
Predicate: GopherMapPredicate } |
||||
textfile := ResponseTransformer{ |
||||
Transformer: TextFileTransformer, |
||||
Predicate: FileTypePlainTextPredicate } |
||||
binary := ResponseTransformer { |
||||
Transformer: BinaryFileTransformer, |
||||
Predicate: FileTypeBinaryPredicate } |
||||
|
||||
c.ResponseTransformers = []ResponseTransformer{ gophermap, textfile, binary } |
||||
|
||||
return c.ResponseTransformers |
||||
} |
||||
|
||||
|
@ -0,0 +1,125 @@ |
||||
package gopher |
||||
|
||||
import ( |
||||
"bufio" |
||||
"regexp" |
||||
"os" |
||||
"fmt" |
||||
"errors" |
||||
|
||||
"github.com/rs/zerolog/log" |
||||
"github.com/gabriel-vasile/mimetype" |
||||
|
||||
) |
||||
|
||||
// non-idiomatic :(
|
||||
// func compose(transformers ...transformer) transformer {
|
||||
// return func(c Context, rq Request, rs Response, e error) (Context, Request, Response, error) {
|
||||
// t:= transformers[0]
|
||||
// ts := transformers[1:]
|
||||
// if len(transformers) == 1 {
|
||||
// return t(c, rq, rs, e)
|
||||
// }
|
||||
|
||||
// return t(compose(ts...)(c, rq, rs, e))
|
||||
// }
|
||||
// }
|
||||
|
||||
func FileType(p string) (ResponseType, error) { |
||||
mimegopher := map[*regexp.Regexp]ResponseType{ |
||||
regexp.MustCompile("text/.*"): PlainText, |
||||
regexp.MustCompile("audio/*"): Audio, |
||||
regexp.MustCompile("image/.*"): Image, |
||||
regexp.MustCompile(".*/pdf"): PDF, |
||||
regexp.MustCompile(".*/msword"): Doc, |
||||
} |
||||
|
||||
mtype, err := mimetype.DetectFile(p) |
||||
|
||||
if err != nil { |
||||
log.Error().Err(err). |
||||
Str("Path", p). |
||||
Msg("Error while detecting a file type") |
||||
} |
||||
|
||||
log.Debug(). |
||||
Str("Filetype", mtype.String()). |
||||
Str("Extension", mtype.Extension()) |
||||
for m, t := range mimegopher { |
||||
if m.MatchString(p) { |
||||
return t, nil |
||||
} |
||||
} |
||||
|
||||
// type not found, default to binary
|
||||
return Binary, nil |
||||
} |
||||
|
||||
func GopherMapLine(c Context, r string, f string, t ResponseType) (string, error) { |
||||
|
||||
s, found := TypePrefix[t] |
||||
|
||||
if !found { |
||||
s = "9" |
||||
} |
||||
|
||||
return s + f + "\t" + r + "/" + f + "\t" + c.Host + "\t" + c.Port + "\r\n", nil |
||||
} |
||||
|
||||
|
||||
func GopherMap(c Context, r string) ([]string, error) { |
||||
g := c.Root + r + "/gophermap" |
||||
if _, err := os.Stat(g); errors.Is(err, os.ErrNotExist) { |
||||
// build a dynamic gophermap listing the contents of the directory
|
||||
entries, err := os.ReadDir(c.Root + r) |
||||
if err != nil { |
||||
log.Error().Err(err). |
||||
Str("Path", r). |
||||
Msg("Error reading the contents of a directory") |
||||
return []string{}, err |
||||
} else { |
||||
log.Debug(). |
||||
Str("Path", r). |
||||
Msg("Generating a gophermap for a directory") |
||||
lines := []string{} |
||||
for _, entry := range entries { |
||||
if entry.IsDir() { |
||||
l, _ := GopherMapLine(c, r, entry.Name(), Directory) |
||||
lines = append(lines, l) |
||||
} else { |
||||
mtype, _ := mimetype.DetectFile(c.Root + r + "/" + entry.Name()) |
||||
t, _ := FileType(mtype.String()) |
||||
l, _ := GopherMapLine(c, r, entry.Name(), t) |
||||
lines = append(lines, l) |
||||
} |
||||
} |
||||
for _, li := range lines { |
||||
log.Debug(). |
||||
Str("GopherMapLine", li). |
||||
Send() |
||||
} |
||||
return lines, nil |
||||
} |
||||
} else { |
||||
return ReadTextFile(g) |
||||
} |
||||
} |
||||
|
||||
func ReadTextFile(p string) ([]string, error) { |
||||
f, err := os.Open(p) |
||||
lines := []string{} |
||||
|
||||
if err != nil { |
||||
return []string{}, fmt.Errorf("Error reading the file %s: %s", p, err.Error()) |
||||
} |
||||
|
||||
fs := bufio.NewScanner(f) |
||||
fs.Split(bufio.ScanLines) |
||||
|
||||
for fs.Scan() { |
||||
lines = append(lines, fs.Text() + "\r\n") |
||||
} |
||||
|
||||
log.Debug().Msg("Done reading file") |
||||
return lines, nil |
||||
} |
@ -0,0 +1,54 @@ |
||||
package gopher |
||||
|
||||
import ( |
||||
"regexp" |
||||
|
||||
"github.com/rs/zerolog/log" |
||||
) |
||||
|
||||
type TransformerPredicate func(Context, Request, Response, error) bool |
||||
|
||||
func FullPathMatchPredicate(c Context, rq Request, rs Response, e error) bool { |
||||
r, e := regexp.Compile(rq.FullPath) |
||||
if e == nil && r.MatchString(rq.FullPath) { |
||||
return true |
||||
} else if e != nil { |
||||
log.Err(e). |
||||
Str("fullpath", rq.FullPath). |
||||
Send() |
||||
} |
||||
|
||||
return false |
||||
} |
||||
|
||||
func GopherMapPredicate(c Context, rq Request, rs Response, e error) bool { |
||||
return rq.Type == Map |
||||
} |
||||
|
||||
func FileTypeBinaryPredicate(c Context, rq Request, rs Response, e error) bool { |
||||
return rs.Type == Binary |
||||
} |
||||
|
||||
func FileTypePlainTextPredicate(c Context, rq Request, rs Response, e error) bool { |
||||
return rq.Type != Map && rs.Type == PlainText |
||||
} |
||||
|
||||
func FileTypeAudioPredicate(c Context, rq Request, rs Response, e error) bool { |
||||
return rs.Type == Audio |
||||
} |
||||
|
||||
func FileTypeImagePredicate(c Context, rq Request, rs Response, e error) bool { |
||||
return rs.Type == Image |
||||
} |
||||
|
||||
func FileTypePDFPredicate(c Context, rq Request, rs Response, e error) bool { |
||||
return rs.Type == PDF |
||||
} |
||||
|
||||
func FileTypeDocPredicate(c Context, rq Request, rs Response, e error) bool { |
||||
return rs.Type == Doc |
||||
} |
||||
|
||||
func FileTypeDirectoryPredicate(c Context, rq Request, rs Response, e error) bool { |
||||
return rq.Type == Directory |
||||
} |
@ -0,0 +1,51 @@ |
||||
package gopher |
||||
|
||||
import ( |
||||
"regexp" |
||||
"os" |
||||
"fmt" |
||||
|
||||
"github.com/rs/zerolog/log" |
||||
) |
||||
|
||||
type RequestType int |
||||
|
||||
const ( |
||||
Map RequestType = iota |
||||
File |
||||
) |
||||
|
||||
type Request struct { |
||||
Path string |
||||
FullPath string |
||||
Type RequestType |
||||
} |
||||
|
||||
|
||||
func NewRequest(c Context, s string) (*Request, error) { |
||||
r := Request{} |
||||
log.Debug(). |
||||
Str("s", s). |
||||
Send() |
||||
ContentList := regexp.MustCompile("^$") |
||||
Selector := regexp.MustCompile("^(/.*)$") |
||||
|
||||
if ContentList.MatchString(s) { |
||||
r.Path = "/" |
||||
r.FullPath = c.Root + "/" |
||||
r.Type = Map |
||||
} else if Selector.MatchString(s) { |
||||
r.Path = s |
||||
r.FullPath = c.Root + s |
||||
fi, _ := os.Stat(r.FullPath) |
||||
if fi.IsDir() { |
||||
r.Type = Map |
||||
} else { |
||||
r.Type = File
|
||||
} |
||||
} else { |
||||
return &r, fmt.Errorf("Unrecognised request: '%s'", s) |
||||
} |
||||
|
||||
return &r, nil |
||||
} |
@ -0,0 +1,73 @@ |
||||
package gopher |
||||
|
||||
import ( |
||||
"github.com/rs/zerolog/log" |
||||
"github.com/gabriel-vasile/mimetype" |
||||
) |
||||
|
||||
type ResponseType int |
||||
|
||||
const ( |
||||
PlainText = iota |
||||
Audio |
||||
Image |
||||
PDF |
||||
Doc |
||||
Binary |
||||
Directory |
||||
) |
||||
|
||||
|
||||
var TypePrefix = map[ResponseType]string{ |
||||
PlainText: "0", |
||||
Audio: "s", |
||||
Image: "I", |
||||
PDF: "P", |
||||
Doc: "d", |
||||
Binary: "9", |
||||
Directory: "1", |
||||
} |
||||
|
||||
|
||||
type Response struct { |
||||
Type ResponseType |
||||
ContentString string |
||||
ContentBinary []byte |
||||
ContentText []string |
||||
} |
||||
|
||||
func NewResponse(c Context, rq Request) *Response { |
||||
r:= Response { Type: Binary, |
||||
ContentText: []string{}, |
||||
ContentBinary: []byte{}, |
||||
ContentString: "", |
||||
} |
||||
|
||||
if rq.Type == Map { |
||||
r.Type = PlainText |
||||
} else { |
||||
mtype, err := mimetype.DetectFile(rq.FullPath) |
||||
if err != nil { |
||||
log.Error(). |
||||
Err(err).
|
||||
Str("FullPath", rq.FullPath). |
||||
Msg("Error detecting a file type") |
||||
} |
||||
|
||||
log.Debug(). |
||||
Str("Filetype:", mtype.String()). |
||||
Str("Extension", mtype.Extension()) |
||||
t, err := FileType(rq.FullPath) |
||||
if err != nil { |
||||
log.Error(). |
||||
Err(err). |
||||
Str("FullPath", rq.FullPath). |
||||
Msg("Error converting a MIME type to a FileType. Defaulting to binary.") |
||||
t = Binary |
||||
} |
||||
|
||||
r.Type = t |
||||
} |
||||
return &r |
||||
} |
||||
|
@ -0,0 +1,76 @@ |
||||
package gopher |
||||
|
||||
import ( |
||||
"bufio" |
||||
"fmt" |
||||
"net" |
||||
|
||||
"github.com/rs/zerolog/log" |
||||
) |
||||
|
||||
// ugly, needs refactoring
|
||||
func write(ctx *Context, rq Request, rs *Response, w *bufio.Writer, c net.Conn) error { |
||||
if rs.Type == PlainText { |
||||
for _, line := range rs.ContentText { |
||||
fmt.Fprint(w, line) |
||||
} |
||||
fmt.Fprint(w, ".\r\n") |
||||
w.Flush() |
||||
} else { |
||||
c.Write(rs.ContentBinary) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func Serve(ctx *Context) { |
||||
l, _ := net.Listen(ctx.TransportProtocol, ":" + ctx.Port)
|
||||
defer l.Close() |
||||
|
||||
for { |
||||
c, err := l.Accept() |
||||
|
||||
if err != nil { |
||||
log.Error(). |
||||
Err(err). |
||||
Msg("Error connecting a client") |
||||
} else { |
||||
s := bufio.NewScanner(bufio.NewReader(c)) |
||||
s.Scan() |
||||
w := bufio.NewWriter(c) |
||||
|
||||
rq, err := NewRequest(*ctx, s.Text()) |
||||
|
||||
if err != nil { |
||||
log.Error(). |
||||
Err(err). |
||||
Msg("Got an error when creating a new Request: ") |
||||
} |
||||
|
||||
log.Debug(). |
||||
Str("Request", rq.Path). |
||||
Send() |
||||
|
||||
rs := NewResponse(*ctx, *rq) |
||||
|
||||
for _, t := range *&ctx.ResponseTransformers { |
||||
if t.Predicate(*ctx, *rq, *rs, err) {
|
||||
ctx, _, rs, err = t.Transformer(ctx, *rq, rs, err) |
||||
} |
||||
} |
||||
|
||||
err = write(ctx, *rq, rs, w, c) |
||||
if err != nil { |
||||
log.Error(). |
||||
Err(err). |
||||
Msg("An error occurred while writing the data on the socket:") |
||||
} |
||||
if err := s.Err(); err != nil { |
||||
log.Error(). |
||||
Err(err). |
||||
Msg("Error reading input") |
||||
} |
||||
} // else
|
||||
|
||||
c.Close() // always close connection
|
||||
} // for
|
||||
} |
@ -0,0 +1,74 @@ |
||||
package gopher |
||||
|
||||
import ( |
||||
"os" |
||||
|
||||
"github.com/rs/zerolog/log" |
||||
) |
||||
|
||||
type ResponseTransformerFunc func(*Context, Request, *Response, error) (*Context, Request, *Response, error) |
||||
|
||||
type ResponseTransformer struct { |
||||
Transformer ResponseTransformerFunc |
||||
Predicate TransformerPredicate |
||||
} |
||||
|
||||
type RequestTransformerFunc func(*Context, *Request, Response, error) (*Context, *Request, Response, error) |
||||
|
||||
type RequestTransformer struct { |
||||
Transformer RequestTransformerFunc |
||||
Predicate TransformerPredicate |
||||
} |
||||
|
||||
func GopherMapTransformer(c *Context, rq Request, rs *Response, e error) (*Context, Request, *Response, error) { |
||||
s, err := GopherMap(*c, rq.Path) |
||||
if err != nil { |
||||
log.Err(err). |
||||
Str("path", rq.Path). |
||||
Send() |
||||
} else { |
||||
rs.ContentText = append(rs.ContentText, s...) |
||||
} |
||||
|
||||
return c, rq, rs, e |
||||
} |
||||
|
||||
func TextFileTransformer(c *Context, rq Request, rs *Response, e error) (*Context, Request, *Response, error) { |
||||
s, err := ReadTextFile(rq.Path) |
||||
if err != nil { |
||||
log.Err(err). |
||||
Str("path", rq.Path). |
||||
Send() |
||||
} else { |
||||
rs.ContentText = append(rs.ContentText, s...) |
||||
} |
||||
|
||||
return c, rq, rs, e |
||||
} |
||||
|
||||
func BinaryFileTransformer(c *Context, rq Request, rs *Response, e error) (*Context, Request, *Response, error) { |
||||
b, err := os.ReadFile(rq.FullPath) |
||||
if err != nil { |
||||
log.Error(). |
||||
Err(err). |
||||
Str("Path", rq.FullPath). |
||||
Msg("Error reading a binary file") |
||||
return c, rq, rs, err |
||||
} |
||||
rs.ContentBinary = append(rs.ContentBinary, b...) |
||||
|
||||
return c, rq, rs, e |
||||
} |
||||
|
||||
func HeaderTransformer(c *Context, rq Request, rs *Response, e error) (*Context, Request, *Response, error) { |
||||
header := []string {} |
||||
for _, h := range c.Header { |
||||
header = append(header, "i" + h + "\r\n") |
||||
} |
||||
rs.ContentText = append(header, rs.ContentText...) |
||||
|
||||
return c, rq, rs, e |
||||
|
||||
} |
||||
|
||||
|
@ -0,0 +1,27 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"beastieboy/marmotte/gopher" |
||||
) |
||||
|
||||
func main() { |
||||
ctx := &gopher.Context{ Root: "/usr/home/kafka/marmotte-root", |
||||
Host: "localhost", |
||||
Port: "7070", |
||||
TransportProtocol: "tcp"} |
||||
|
||||
ctx.DefaultResponseTransformers() |
||||
|
||||
// custom header for gophermaps
|
||||
ctx.Header= []string { |
||||
"Welcome to " + ctx.Host, |
||||
"This gopherhole is powered by marmotte"} |
||||
|
||||
ctx.ResponseTransformers = append(ctx.ResponseTransformers, gopher.ResponseTransformer{ |
||||
Transformer: gopher.HeaderTransformer, |
||||
Predicate: gopher.GopherMapPredicate }) |
||||
|
||||
gopher.Serve(ctx) |
||||
|
||||
} |
||||
|
Loading…
Reference in new issue