initial
This commit is contained in:
commit
4103e14aec
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
*.crt
|
||||||
|
*.key
|
||||||
|
*.yaml
|
||||||
|
*.sum
|
19
go.mod
Normal file
19
go.mod
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
module gemineye
|
||||||
|
|
||||||
|
go 1.13
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/chewxy/sexp v0.0.0-20181223234510-461851156c0f // indirect
|
||||||
|
github.com/gorilla/mux v1.7.4 // indirect
|
||||||
|
github.com/pelletier/go-toml v1.6.0 // indirect
|
||||||
|
github.com/spf13/afero v1.2.2 // indirect
|
||||||
|
github.com/spf13/cast v1.3.1 // indirect
|
||||||
|
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/spf13/viper v1.6.2
|
||||||
|
github.com/stryan/go-gopher v0.0.0-20191008152201-adba90945dcb
|
||||||
|
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 // indirect
|
||||||
|
golang.org/x/text v0.3.2 // indirect
|
||||||
|
gopkg.in/ini.v1 v1.52.0 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.2.8 // indirect
|
||||||
|
)
|
44
main.go
Normal file
44
main.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
viper.SetConfigName("config")
|
||||||
|
viper.AddConfigPath("/etc/gemineye/")
|
||||||
|
viper.AddConfigPath(".")
|
||||||
|
err := viper.ReadInConfig()
|
||||||
|
if err != nil { // Handle errors reading the config file
|
||||||
|
log.Fatalf("Fatal error config file: %v \n", err)
|
||||||
|
}
|
||||||
|
viper.SetConfigType("yaml")
|
||||||
|
|
||||||
|
//Load config
|
||||||
|
active_capsules := viper.GetStringSlice("active_capsules")
|
||||||
|
capsule_list := make([]Config, len(active_capsules))
|
||||||
|
for i, c := range active_capsules {
|
||||||
|
viper.UnmarshalKey(c, &(capsule_list[i]))
|
||||||
|
}
|
||||||
|
if len(capsule_list) < 1 {
|
||||||
|
log.Println("No capsules defined. Shutting down.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Fatal(ListenAndServeTLS(capsule_list[0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Hostname string
|
||||||
|
Port string
|
||||||
|
KeyFile string
|
||||||
|
CertFile string
|
||||||
|
RootDir string
|
||||||
|
CGIDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) String() string {
|
||||||
|
return fmt.Sprintf("Config: %v:%v Files:%v CGI:%v", c.Hostname, c.Port, c.RootDir, c.CGIDir)
|
||||||
|
}
|
45
proto.go
Normal file
45
proto.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
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 Request struct {
|
||||||
|
Hostname string
|
||||||
|
Port string
|
||||||
|
Path string
|
||||||
|
Params string
|
||||||
|
Query string
|
||||||
|
Fragment string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
Status int
|
||||||
|
Meta string
|
||||||
|
Body string
|
||||||
|
}
|
232
server.go
Normal file
232
server.go
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"mime"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
type contextKey struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *contextKey) String() string {
|
||||||
|
return "gemini context value " + k.name
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ServerContextKey is a context key. It can be used in Gemini
|
||||||
|
// handlers with context.WithValue to access the server that
|
||||||
|
// started the handler. The associated value will be of type *Server.
|
||||||
|
ServerContextKey = &contextKey{"gemini-server"}
|
||||||
|
|
||||||
|
// LocalAddrContextKey is a context key. It can be used in
|
||||||
|
// Gopher handlers with context.WithValue to access the address
|
||||||
|
// the local address the connection arrived on.
|
||||||
|
// The associated value will be of type net.Addr.
|
||||||
|
LocalAddrContextKey = &contextKey{"local-addr"}
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
Addr string // TCP address to listen on, ":gemini" if empty
|
||||||
|
|
||||||
|
Hostname string // FQDN Hostname to reach this server on
|
||||||
|
ServerRoot string //Root folder for gemini files
|
||||||
|
}
|
||||||
|
|
||||||
|
type conn struct {
|
||||||
|
server *Server
|
||||||
|
C net.Conn
|
||||||
|
tlsState *tls.ConnectionState
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) newConn(rwc net.Conn) *conn {
|
||||||
|
c := &conn{
|
||||||
|
server: s,
|
||||||
|
C: rwc,
|
||||||
|
tlsState: nil,
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
func (s *Server) ListenAndServeTLS(certFile, keyFile string) error {
|
||||||
|
addr := s.Addr
|
||||||
|
mime.AddExtensionType(".gmi", "text/gemini")
|
||||||
|
mime.AddExtensionType(".gemini", "text/gemini")
|
||||||
|
if addr == "" {
|
||||||
|
addr = ":1965"
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("server: loadkeys: %s", err)
|
||||||
|
}
|
||||||
|
config := tls.Config{Certificates: []tls.Certificate{cert}}
|
||||||
|
config.Rand = rand.Reader
|
||||||
|
|
||||||
|
ln, err := tls.Listen("tcp", addr, &config)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("server: listen: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Serve(ln)
|
||||||
|
}
|
||||||
|
func ListenAndServeTLS(cp Config) error {
|
||||||
|
server := &Server{Addr: ":" + cp.Port, ServerRoot: cp.RootDir, Hostname: cp.Hostname}
|
||||||
|
return server.ListenAndServeTLS(cp.CertFile, cp.KeyFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Serve(l net.Listener) error {
|
||||||
|
defer l.Close()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
ctx = context.WithValue(ctx, ServerContextKey, s)
|
||||||
|
ctx = context.WithValue(ctx, LocalAddrContextKey, l.Addr())
|
||||||
|
|
||||||
|
for {
|
||||||
|
rw, err := l.Accept()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Errorf("error accepting new client: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c := s.newConn(rw)
|
||||||
|
go c.serve(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) serve(ctx context.Context) {
|
||||||
|
buf := bufio.NewReader(c.C)
|
||||||
|
data := make([]byte, 1026) //1024 for the URL, 2 for the CRLF
|
||||||
|
//req, overflow, err := req_buf.ReadLine()
|
||||||
|
count := 0
|
||||||
|
for count < 1026 {
|
||||||
|
if strings.Contains(string(data), "\r\n") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
b, err := buf.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("WARN: Couldn't serve request: %v", err.Error())
|
||||||
|
c.C.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data[count] = b
|
||||||
|
count = count + 1
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(data), "\r\n") {
|
||||||
|
c.sendResponse(Response{STATUS_BAD_REQUEST, "Request too large", ""})
|
||||||
|
c.C.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !utf8.Valid(data) {
|
||||||
|
c.sendResponse(Response{STATUS_BAD_REQUEST, "URL contains non UTF8 charcaters", ""})
|
||||||
|
c.C.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req := string(data[:count-2])
|
||||||
|
res := c.server.ParseRequest(req)
|
||||||
|
c.sendResponse(res)
|
||||||
|
c.C.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) ParseRequest(req string) Response {
|
||||||
|
u, err := url.Parse(req)
|
||||||
|
if err != nil {
|
||||||
|
return Response{STATUS_BAD_REQUEST, "URL invalid", ""}
|
||||||
|
}
|
||||||
|
if u.Scheme == "" {
|
||||||
|
u.Scheme = "gemini"
|
||||||
|
} else if u.Scheme != "gemini" {
|
||||||
|
return Response{STATUS_PROXY_REQUEST_REFUSED, "Proxying by Scheme not currently supported", ""}
|
||||||
|
}
|
||||||
|
if u.Port() != "1965" && u.Port() != "" {
|
||||||
|
return Response{STATUS_PROXY_REQUEST_REFUSED, "Proxying by Port not currently supported", ""}
|
||||||
|
}
|
||||||
|
if u.Host == "" {
|
||||||
|
return Response{STATUS_BAD_REQUEST, "Need to specify a host", ""}
|
||||||
|
} else if u.Hostname() != s.Hostname {
|
||||||
|
return Response{STATUS_PROXY_REQUEST_REFUSED, "Proxying by Hostname not currently supported", ""}
|
||||||
|
}
|
||||||
|
if strings.Contains(u.Path, "..") {
|
||||||
|
return Response{STATUS_PERMANENT_FAILURE, "Dots in path, assuming bad faith.", ""}
|
||||||
|
}
|
||||||
|
|
||||||
|
selector := s.ServerRoot + u.Path
|
||||||
|
fi, err := os.Stat(selector)
|
||||||
|
switch {
|
||||||
|
case err != nil:
|
||||||
|
// File doesn't exist.
|
||||||
|
return Response{STATUS_NOT_FOUND, "Couldn't find file", ""}
|
||||||
|
case os.IsNotExist(err) || os.IsPermission(err):
|
||||||
|
return Response{STATUS_NOT_FOUND, "File does not exist", ""}
|
||||||
|
case uint64(fi.Mode().Perm())&0444 != 0444:
|
||||||
|
return Response{STATUS_TEMPORARY_FAILURE, "Unable to access file", ""}
|
||||||
|
case fi.IsDir():
|
||||||
|
if strings.HasSuffix(u.Path, "/") {
|
||||||
|
return generateDirectoryListing(selector)
|
||||||
|
} else {
|
||||||
|
return Response{STATUS_REDIRECT_PERMANENT, "gemini://" + s.Hostname + u.Path + "/", ""}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// it's a file
|
||||||
|
meta := mime.TypeByExtension(filepath.Ext(selector))
|
||||||
|
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 generateDirectoryListing(path string) Response {
|
||||||
|
var listing string
|
||||||
|
files, err := ioutil.ReadDir(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return Response{STATUS_TEMPORARY_FAILURE, "Unable to show directory listing", ""}
|
||||||
|
}
|
||||||
|
listing = "# Directory listing\r\n"
|
||||||
|
for _, file := range files {
|
||||||
|
// Skip dotfiles
|
||||||
|
if strings.HasPrefix(file.Name(), ".") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Only list world readable files
|
||||||
|
if uint64(file.Mode().Perm())&0444 != 0444 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if file.Name() == "index.gmi" || file.Name() == "index.gemini" {
|
||||||
|
//Found an index file, return that instead
|
||||||
|
read_file, err := os.Open(path + file.Name())
|
||||||
|
if err != nil {
|
||||||
|
return Response{STATUS_TEMPORARY_FAILURE, "Unable to show directory index file", ""}
|
||||||
|
}
|
||||||
|
defer read_file.Close()
|
||||||
|
buf, err := ioutil.ReadAll(read_file)
|
||||||
|
return Response{STATUS_SUCCESS, "text/gemini", string(buf)}
|
||||||
|
} else {
|
||||||
|
listing += fmt.Sprintf("=> %s %s\r\n", file.Name(), file.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Response{STATUS_SUCCESS, "text/gemini", listing}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) sendResponse(r Response) error {
|
||||||
|
c.C.Write([]byte(fmt.Sprintf("%v %v\r\n", r.Status, r.Meta)))
|
||||||
|
if r.Body != "" {
|
||||||
|
c.C.Write([]byte(r.Body))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user