add CGI parsing
This commit is contained in:
parent
58d983891e
commit
e09110064b
150
gemini.go
Normal file
150
gemini.go
Normal file
@ -0,0 +1,150 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"mime"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Yoinked from jetforce and go'ified
|
||||
const (
|
||||
STATUS_INPUT = 10
|
||||
|
||||
STATUS_SUCCESS = 20
|
||||
STATUS_SUCCESS_END_OF_SESSION = 21
|
||||
|
||||
STATUS_REDIRECT_TEMPORARY = 30
|
||||
STATUS_REDIRECT_PERMANENT = 31
|
||||
|
||||
STATUS_TEMPORARY_FAILURE = 40
|
||||
STATUS_SERVER_UNAVAILABLE = 41
|
||||
STATUS_CGI_ERROR = 42
|
||||
STATUS_PROXY_ERROR = 43
|
||||
STATUS_SLOW_DOWN = 44
|
||||
|
||||
STATUS_PERMANENT_FAILURE = 50
|
||||
STATUS_NOT_FOUND = 51
|
||||
STATUS_GONE = 52
|
||||
STATUS_PROXY_REQUEST_REFUSED = 53
|
||||
STATUS_BAD_REQUEST = 59
|
||||
|
||||
STATUS_CLIENT_CERTIFICATE_REQUIRED = 60
|
||||
STATUS_TRANSIENT_CERTIFICATE_REQUESTED = 61
|
||||
STATUS_AUTHORISED_CERTIFICATE_REQUIRED = 62
|
||||
STATUS_CERTIFICATE_NOT_ACCEPTED = 63
|
||||
STATUS_FUTURE_CERTIFICATE_REJECTED = 64
|
||||
STATUS_EXPIRED_CERTIFICATE_REJECTED = 65
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
Status int
|
||||
Meta string
|
||||
Body string
|
||||
}
|
||||
|
||||
type GeminiConfig struct {
|
||||
Hostname string
|
||||
KeyFile string
|
||||
CertFile string
|
||||
RootDir string
|
||||
CGIDir string
|
||||
}
|
||||
|
||||
func (c *GeminiConfig) String() string {
|
||||
return fmt.Sprintf("Gemini Config: %v Files:%v CGI:%v", c.Hostname, c.RootDir, c.CGIDir)
|
||||
}
|
||||
|
||||
func generateFile(selector string) Response {
|
||||
meta := mime.TypeByExtension(filepath.Ext(selector))
|
||||
if meta == "" {
|
||||
//assume plain UTF-8 text
|
||||
meta = "text/gemini; charset=utf-8"
|
||||
}
|
||||
file, err := os.Open(selector)
|
||||
if err != nil {
|
||||
panic("Failed to read file")
|
||||
}
|
||||
defer file.Close()
|
||||
buf, err := ioutil.ReadAll(file)
|
||||
return Response{STATUS_SUCCESS, meta, string(buf)}
|
||||
}
|
||||
|
||||
func generateDirectory(path string) Response {
|
||||
var dirpage string
|
||||
files, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return Response{STATUS_TEMPORARY_FAILURE, "Unable to show directory dirpage", ""}
|
||||
}
|
||||
dirpage = "# Directory Contents\r\n"
|
||||
for _, file := range files {
|
||||
// Don't list hidden files
|
||||
if isNotWorldReadable(file) || strings.HasPrefix(file.Name(), ".") {
|
||||
continue
|
||||
}
|
||||
if file.Name() == "index.gmi" || file.Name() == "index.gemini" {
|
||||
//Found an index file, return that instead
|
||||
return generateFile(path + file.Name())
|
||||
} else {
|
||||
dirpage += fmt.Sprintf("=> %s %s\r\n", file.Name(), file.Name())
|
||||
}
|
||||
}
|
||||
return Response{STATUS_SUCCESS, "text/gemini", dirpage}
|
||||
}
|
||||
|
||||
func generateCGI(selector *url.URL, c *conn) Response {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) //make this customizable
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, selector.Path)
|
||||
|
||||
//build CGI environment
|
||||
cmd.Env = []string{fmt.Sprintf("GEMINI_URL=%v", selector),
|
||||
fmt.Sprintf("HOSTNAME=%v", selector.Hostname()),
|
||||
fmt.Sprintf("PATH_INFO=%v", selector.Path),
|
||||
fmt.Sprintf("QUERY_STRING=%v", selector.RawQuery),
|
||||
fmt.Sprintf("REMOTE_ADDR=%v", c.C.RemoteAddr()),
|
||||
fmt.Sprintf("REMOTE_HOST=%v", c.C.RemoteAddr()),
|
||||
fmt.Sprintf("SERVER_NAME=%v", selector.Hostname()),
|
||||
fmt.Sprintf("SERVER_PORT=%v", c.server.Port),
|
||||
"SERVER_PROTOCOL=GEMINI",
|
||||
"SERVER_SOFTWARE=secretshop/0.1.0",
|
||||
}
|
||||
cmdout, err := cmd.Output()
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
log.Printf("CGI %v timed out", selector)
|
||||
return Response{STATUS_CGI_ERROR, "CGI process timed out", ""}
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("Error running CGI process %v", selector)
|
||||
return Response{STATUS_CGI_ERROR, "Error running CGI process", ""}
|
||||
}
|
||||
|
||||
header, _, err := bufio.NewReader(strings.NewReader(string(cmdout))).ReadLine()
|
||||
if err != nil {
|
||||
log.Printf("Error running CGI process %v", selector)
|
||||
return Response{STATUS_CGI_ERROR, "Error running CGI process", ""}
|
||||
}
|
||||
header_s := strings.Fields(string(header))
|
||||
//make sure it has a valid status
|
||||
status, err := strconv.Atoi(header_s[0])
|
||||
if err != nil {
|
||||
log.Printf("Error running CGI process %v", selector)
|
||||
return Response{STATUS_CGI_ERROR, "Error running CGI process", ""}
|
||||
}
|
||||
if status < 0 || status > 69 {
|
||||
log.Printf("CGI script returned bad status %v", selector)
|
||||
return Response{STATUS_CGI_ERROR, "Error running CGI process", ""}
|
||||
}
|
||||
return Response{status, "", string(cmdout)}
|
||||
}
|
51
proto.go
51
proto.go
@ -1,51 +0,0 @@
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Yoinked from jetforce and go'ified
|
||||
const (
|
||||
STATUS_INPUT = 10
|
||||
|
||||
STATUS_SUCCESS = 20
|
||||
STATUS_SUCCESS_END_OF_SESSION = 21
|
||||
|
||||
STATUS_REDIRECT_TEMPORARY = 30
|
||||
STATUS_REDIRECT_PERMANENT = 31
|
||||
|
||||
STATUS_TEMPORARY_FAILURE = 40
|
||||
STATUS_SERVER_UNAVAILABLE = 41
|
||||
STATUS_CGI_ERROR = 42
|
||||
STATUS_PROXY_ERROR = 43
|
||||
STATUS_SLOW_DOWN = 44
|
||||
|
||||
STATUS_PERMANENT_FAILURE = 50
|
||||
STATUS_NOT_FOUND = 51
|
||||
STATUS_GONE = 52
|
||||
STATUS_PROXY_REQUEST_REFUSED = 53
|
||||
STATUS_BAD_REQUEST = 59
|
||||
|
||||
STATUS_CLIENT_CERTIFICATE_REQUIRED = 60
|
||||
STATUS_TRANSIENT_CERTIFICATE_REQUESTED = 61
|
||||
STATUS_AUTHORISED_CERTIFICATE_REQUIRED = 62
|
||||
STATUS_CERTIFICATE_NOT_ACCEPTED = 63
|
||||
STATUS_FUTURE_CERTIFICATE_REJECTED = 64
|
||||
STATUS_EXPIRED_CERTIFICATE_REJECTED = 65
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
Status int
|
||||
Meta string
|
||||
Body string
|
||||
}
|
||||
|
||||
type GeminiConfig struct {
|
||||
Hostname string
|
||||
KeyFile string
|
||||
CertFile string
|
||||
RootDir string
|
||||
CGIDir string
|
||||
}
|
||||
|
||||
func (c *GeminiConfig) String() string {
|
||||
return fmt.Sprintf("Gemini Config: %v Files:%v CGI:%v", c.Hostname, c.RootDir, c.CGIDir)
|
||||
}
|
57
server.go
57
server.go
@ -6,7 +6,6 @@ import (
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"mime"
|
||||
"net"
|
||||
@ -40,8 +39,10 @@ var (
|
||||
|
||||
type Server struct {
|
||||
Addr string // TCP address to listen on, ":gemini" if empty
|
||||
Port string // TCP port
|
||||
|
||||
HostnameToRoot map[string]string //FQDN hostname to root folder
|
||||
HostnameToCGI map[string]string //FQDN hostname to CGI folder
|
||||
}
|
||||
|
||||
type conn struct {
|
||||
@ -60,9 +61,10 @@ func (s *Server) newConn(rwc net.Conn) *conn {
|
||||
}
|
||||
|
||||
func ListenAndServeTLS(port string, cps []GeminiConfig) error {
|
||||
server := &Server{Addr: ":" + port, HostnameToRoot: make(map[string]string)}
|
||||
server := &Server{Addr: ":" + port, Port: port, HostnameToRoot: make(map[string]string), HostnameToCGI: make(map[string]string)}
|
||||
for _, c := range cps {
|
||||
server.HostnameToRoot[c.Hostname] = c.RootDir
|
||||
server.HostnameToCGI[c.Hostname] = c.CGIDir
|
||||
}
|
||||
return server.ListenAndServeTLS(cps)
|
||||
}
|
||||
@ -140,14 +142,14 @@ func (c *conn) serve(ctx context.Context) {
|
||||
res = Response{STATUS_BAD_REQUEST, "URL contains non UTF8 charcaters", ""}
|
||||
} else {
|
||||
req = string(data[:count-2])
|
||||
res = c.server.ParseRequest(req)
|
||||
res = c.server.ParseRequest(req, c)
|
||||
}
|
||||
c.sendResponse(res)
|
||||
log.Printf("%v requested %v; responded with %v %v", c.C.RemoteAddr(), req, res.Status, res.Meta)
|
||||
c.C.Close()
|
||||
}
|
||||
|
||||
func (s *Server) ParseRequest(req string) Response {
|
||||
func (s *Server) ParseRequest(req string, c *conn) Response {
|
||||
u, err := url.Parse(req)
|
||||
if err != nil {
|
||||
return Response{STATUS_BAD_REQUEST, "URL invalid", ""}
|
||||
@ -187,46 +189,19 @@ func (s *Server) ParseRequest(req string) Response {
|
||||
}
|
||||
default:
|
||||
// it's a file
|
||||
matches, err := filepath.Glob(s.HostnameToCGI[u.Hostname()] + "/*")
|
||||
if err != nil {
|
||||
log.Printf("%v: Couldn't search for CGI: %v", u.Hostname(), err)
|
||||
return Response{STATUS_TEMPORARY_FAILURE, "Error finding file", ""}
|
||||
}
|
||||
if matches != nil && fi.Mode().Perm()&0111 == 0111 {
|
||||
//CGI file found
|
||||
return generateCGI(u, c)
|
||||
} else {
|
||||
//Normal file found
|
||||
return generateFile(selector)
|
||||
}
|
||||
}
|
||||
|
||||
func generateFile(selector string) Response {
|
||||
meta := mime.TypeByExtension(filepath.Ext(selector))
|
||||
if meta == "" {
|
||||
//assume plain UTF-8 text
|
||||
meta = "text/gemini; charset=utf-8"
|
||||
}
|
||||
file, err := os.Open(selector)
|
||||
if err != nil {
|
||||
panic("Failed to read file")
|
||||
}
|
||||
defer file.Close()
|
||||
buf, err := ioutil.ReadAll(file)
|
||||
return Response{STATUS_SUCCESS, meta, string(buf)}
|
||||
}
|
||||
|
||||
func generateDirectory(path string) Response {
|
||||
var dirpage string
|
||||
files, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return Response{STATUS_TEMPORARY_FAILURE, "Unable to show directory dirpage", ""}
|
||||
}
|
||||
dirpage = "# Directory Contents\r\n"
|
||||
for _, file := range files {
|
||||
// Don't list hidden files
|
||||
if isNotWorldReadable(file) || strings.HasPrefix(file.Name(), ".") {
|
||||
continue
|
||||
}
|
||||
if file.Name() == "index.gmi" || file.Name() == "index.gemini" {
|
||||
//Found an index file, return that instead
|
||||
return generateFile(path + file.Name())
|
||||
} else {
|
||||
dirpage += fmt.Sprintf("=> %s %s\r\n", file.Name(), file.Name())
|
||||
}
|
||||
}
|
||||
return Response{STATUS_SUCCESS, "text/gemini", dirpage}
|
||||
}
|
||||
|
||||
func (c *conn) sendResponse(r Response) error {
|
||||
|
Loading…
Reference in New Issue
Block a user