You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
gophi/gopher/gophermap.go

258 lines
6.7 KiB
Go

package gopher
import (
"gophi/core"
"os"
"github.com/grufwub/go-errors"
)
// GophermapSection is an interface that specifies individually renderable (and writeable) sections of a gophermap
type gophermapSection interface {
RenderAndWrite(*core.Client) error
}
// readGophermap reads a FD and Path as gophermap sections
func readGophermap(file *os.File, p *core.Path) ([]gophermapSection, error) {
// Create return slice
sections := make([]gophermapSection, 0)
// Create hidden files map now in case later requested
hidden := map[string]bool{
p.Relative(): true,
}
// Declare variables
var returnErr error
titleAlready := false
// Perform scan of gophermap FD
scanErr := core.ScanFile(
file,
func(line string) bool {
// Parse the line item type and handle
lineType := parseLineType(line)
switch lineType {
case typeInfoNotStated:
// Append TypeInfo to beginning of line
sections = append(sections, &TextSection{buildInfoLine(line)})
return true
case typeTitle:
// Reformat title line to send as info line with appropriate selector
if !titleAlready {
sections = append(sections, &TextSection{buildLine(typeInfo, line[1:], "TITLE", nullHost, nullPort)})
titleAlready = true
return true
}
returnErr = errInvalidGophermap.Extendf("%s multiple title declarations", p.Absolute())
return false
case typeComment:
// ignore this line
return true
case typeHiddenFile:
// Add to hidden files map
hidden[line[1:]] = true
return true
case typeSubGophermap:
// Parse encoded URI
path, query, returnErr := core.ParseEncodedURI(line[1:])
if returnErr != nil {
return false
}
// Build new request. If empty relative path, or relative
// equal to current gophermap (recurse!!!) we return error
request := core.NewRequest(core.BuildPath(path), query)
if request.Path().Relative() == "" || request.Path().Relative() == p.Relative() {
returnErr = errInvalidGophermap.Extendf("%s invalid subgophermap '%s'", p.Absolute(), request.Path().Absolute())
return false
}
// Open sub gophermap
var subFile *os.File
subFile, returnErr = core.OpenFile(request.Path())
if returnErr != nil {
return false
}
// Get stat
stat, err := subFile.Stat()
if err != nil {
returnErr = errors.With(err).WrapWithin(core.ErrFileStat)
return false
} else if stat.IsDir() {
returnErr = errSubgophermapIsDir.Extend(request.Path().Absolute())
return false
}
// Handle CGI script
if core.WithinCGIDir(request.Path()) {
sections = append(sections, &CGISection{request})
return true
}
// Error out if file too big
if stat.Size() > subgophermapSizeMax {
returnErr = errSubgophermapSize.Extendf("%s %.2fMB", request.Path().Absolute(), stat.Size()/1000.0)
return false
}
// Handle regular file
if !isGophermap(request.Path()) {
sections = append(sections, &FileSection{request.Path()})
return true
}
// Handle gophermap
sections = append(sections, &SubgophermapSection{request.Path()})
return true
case typeEnd:
// Last line, break-out!
return false
case typeEndBeginList:
// Append DirectorySection object then break, as-with typeEnd
dirPath := p.Dir()
sections = append(sections, &DirectorySection{hidden, dirPath})
return false
default:
// Default is appending line with replaced strings to sections slice as TextSection
sections = append(sections, &TextSection{[]byte(replacePlacementStrs(line) + "\r\n")})
return true
}
},
)
// Check the scan didn't exit with error
if returnErr != nil {
return nil, returnErr
} else if scanErr != nil {
return nil, scanErr
}
return sections, nil
}
// TextSection is a simple implementation that holds line's byte contents as-is
type TextSection struct {
contents []byte
}
// RenderAndWrite simply writes the byte slice to the client
func (s *TextSection) RenderAndWrite(client *core.Client) error {
return client.Conn().Write(s.contents)
}
// DirectorySection is an implementation that holds a dir path, and map of hidden files, to later list a dir contents
type DirectorySection struct {
hidden map[string]bool
path *core.Path
}
// RenderAndWrite scans and renders a list of the contents of a directory (skipping hidden or restricted files)
func (s *DirectorySection) RenderAndWrite(client *core.Client) error {
file, err := core.OpenFile(s.path)
if err != nil {
return err
}
// Slice to write
dirContents := make([]byte, 0)
// Scan directory and build lines
err = core.ScanDirectory(file, s.path, func(file os.FileInfo, p *core.Path) {
// Ignore hidden files!
_, ok := s.hidden[file.Name()]
if ok {
return
}
// Append new formatted file listing (if correct type)
dirContents = appendFileListing(dirContents, file, p)
})
if err != nil {
return err
}
// Write dirContents to client
return client.Conn().Write(dirContents)
}
// FileSection is an implementation that holds a file path, and writes the file contents to client
type FileSection struct {
path *core.Path
}
// RenderAndWrite simply opens, reads and writes the file contents to the client
func (s *FileSection) RenderAndWrite(client *core.Client) error {
// Open FD for the file
file, err := core.OpenFile(s.path)
if err != nil {
return err
}
// Byte slice to contain gophermap contents
b := make([]byte, 0)
// Scan the file contents, format for gophermap, append to byte slice
err = core.ScanFile(
file,
func(line string) bool {
b = append(b, buildInfoLine(line)...)
return true
},
)
if err != nil {
return err
}
// Write the file contents to the client
return client.Conn().Write(b)
}
// SubgophermapSection is an implementation to hold onto a gophermap path, then read, render and write contents to a client
type SubgophermapSection struct {
path *core.Path
}
// RenderAndWrite reads, renders and writes the contents of the gophermap to the client
func (s *SubgophermapSection) RenderAndWrite(client *core.Client) error {
// Get FD for gophermap
file, err := core.OpenFile(s.path)
if err != nil {
return err
}
// Read gophermap into sections
sections, err := readGophermap(file, s.path)
if err != nil {
return err
}
// Write each of the sections (AAAA COULD BE RECURSIONNNNN)
for _, section := range sections {
err := section.RenderAndWrite(client)
if err != nil {
return err
}
}
return nil
}
// CGISection is an implementation that holds onto a built request, then processing as a CGI request on request
type CGISection struct {
request *core.Request
}
// RenderAndWrite takes the request, and executes the associated CGI script with parameters
func (s *CGISection) RenderAndWrite(client *core.Client) error {
return core.TryExecuteCGIScript(client, s.request)
}