Added basic functionality and types

pull/4/head
Nicolas Herry 1 year ago
parent 8da7501544
commit d8c6a7cf20
  1. 12
      go.mod
  2. 26
      go.sum
  3. 56
      gopher/context.go
  4. 125
      gopher/files.go
  5. 54
      gopher/predicates.go
  6. 51
      gopher/request.go
  7. 73
      gopher/response.go
  8. 76
      gopher/server.go
  9. 74
      gopher/transformers.go
  10. 27
      main.go

@ -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…
Cancel
Save