update docs
This commit is contained in:
parent
c516b40d74
commit
d7b3b1ca45
@ -46,4 +46,5 @@ Simply run "make uninstall".
|
|||||||
Either run the executable directly or use the SystemD unit file
|
Either run the executable directly or use the SystemD unit file
|
||||||
|
|
||||||
## Caveats
|
## Caveats
|
||||||
Currently does not support cert-based authorization.
|
Currently does not support transient certificates
|
||||||
|
Only handles whitelisting for certificate authorization
|
||||||
|
@ -11,6 +11,14 @@ localhost:
|
|||||||
CGIDir: "/var/gemini/cgi"
|
CGIDir: "/var/gemini/cgi"
|
||||||
KeyFile: "localhost.key"
|
KeyFile: "localhost.key"
|
||||||
CertFile: "localhost.crt"
|
CertFile: "localhost.crt"
|
||||||
|
AccessControl:
|
||||||
|
Identified:
|
||||||
|
- /id
|
||||||
|
Known:
|
||||||
|
- /known
|
||||||
|
Trusted:
|
||||||
|
- /private
|
||||||
|
Whitelist: "whitelist"
|
||||||
localhost2:
|
localhost2:
|
||||||
Hostname: "gemini.foo.bar"
|
Hostname: "gemini.foo.bar"
|
||||||
RootDir: "/var/gemini2"
|
RootDir: "/var/gemini2"
|
||||||
|
150
gemini.go
150
gemini.go
@ -1,150 +0,0 @@
|
|||||||
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)}
|
|
||||||
}
|
|
39
gopher.go
39
gopher.go
@ -1,39 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"github.com/prologic/go-gopher"
|
|
||||||
)
|
|
||||||
|
|
||||||
type GopherConfig struct {
|
|
||||||
Hostname string
|
|
||||||
Port string
|
|
||||||
RootDir string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *GopherConfig) String() string {
|
|
||||||
return fmt.Sprintf("Gopher Config: %v:%v Files:%v", c.Hostname, c.Port, c.RootDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
type indexHandler struct {
|
|
||||||
rootPath string
|
|
||||||
rootHandler gopher.Handler
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *indexHandler) ServeGopher(w gopher.ResponseWriter, r *gopher.Request) {
|
|
||||||
upath := r.Selector
|
|
||||||
if gopher.GetItemType(f.rootPath+upath) == gopher.DIRECTORY && upath != "/" {
|
|
||||||
w.WriteItem(&gopher.Item{
|
|
||||||
Type: gopher.DIRECTORY,
|
|
||||||
Selector: path.Dir(upath),
|
|
||||||
Description: "Go Back",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
f.rootHandler.ServeGopher(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func index(root gopher.FileSystem) *indexHandler {
|
|
||||||
return &indexHandler{root.Name(), gopher.FileServer(root)}
|
|
||||||
}
|
|
64
main.go
64
main.go
@ -1,64 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/prologic/go-gopher"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
viper.SetConfigName("config")
|
|
||||||
viper.AddConfigPath("/etc/secretshop/")
|
|
||||||
viper.AddConfigPath(".")
|
|
||||||
err := viper.ReadInConfig()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Fatal error config file: %v \n", err)
|
|
||||||
}
|
|
||||||
viper.SetConfigType("yaml")
|
|
||||||
|
|
||||||
//Load configs
|
|
||||||
active_capsules := viper.GetStringSlice("active_capsules")
|
|
||||||
active_holes := viper.GetStringSlice("active_holes")
|
|
||||||
port := viper.GetString("port")
|
|
||||||
capsule_list := make([]GeminiConfig, len(active_capsules))
|
|
||||||
hole_list := make([]GopherConfig, len(active_holes))
|
|
||||||
for i, c := range active_capsules {
|
|
||||||
viper.UnmarshalKey(c, &(capsule_list[i]))
|
|
||||||
log.Printf("Loading capsule %v %v", i, capsule_list[i].Hostname)
|
|
||||||
}
|
|
||||||
for i, h := range active_holes {
|
|
||||||
viper.UnmarshalKey(h, &(hole_list[i]))
|
|
||||||
log.Printf("Loading hole %v %v", i, hole_list[i].Hostname)
|
|
||||||
}
|
|
||||||
if len(capsule_list) < 1 && len(hole_list) < 1 {
|
|
||||||
log.Println("No capsules or gopherholes loaded. Shutting down.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Printf("%v capsules loaded, %v gopherholes loaded", len(capsule_list), len(hole_list))
|
|
||||||
// Intialize servers
|
|
||||||
wg := new(sync.WaitGroup)
|
|
||||||
wg.Add(1 + len(hole_list))
|
|
||||||
|
|
||||||
log.Printf("Starting gemini capsule")
|
|
||||||
go func(c interface{}) {
|
|
||||||
log.Fatal(ListenAndServeTLS(port, c.([]GeminiConfig)))
|
|
||||||
wg.Done()
|
|
||||||
}(capsule_list)
|
|
||||||
|
|
||||||
for i, h := range hole_list {
|
|
||||||
log.Printf("Starting gopherhole %v %v", i, h.Hostname)
|
|
||||||
go func(h interface{}) {
|
|
||||||
hole := h.(GopherConfig)
|
|
||||||
gopher.Handle("/", index(gopher.Dir(hole.RootDir)))
|
|
||||||
server := &gopher.Server{Addr: "0.0.0.0:" + hole.Port, Hostname: hole.Hostname, Handler: nil}
|
|
||||||
log.Fatal(server.ListenAndServe())
|
|
||||||
wg.Done()
|
|
||||||
}(h)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("Done bringing up capsules and gopherholes")
|
|
||||||
log.Println("Ho ho! You found me!")
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
217
server.go
217
server.go
@ -1,217 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/tls"
|
|
||||||
"fmt"
|
|
||||||
"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
|
|
||||||
// Gemini 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
|
|
||||||
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 {
|
|
||||||
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 ListenAndServeTLS(port string, cps []GeminiConfig) error {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) ListenAndServeTLS(configs []GeminiConfig) error {
|
|
||||||
addr := s.Addr
|
|
||||||
mime.AddExtensionType(".gmi", "text/gemini")
|
|
||||||
mime.AddExtensionType(".gemini", "text/gemini")
|
|
||||||
if addr == "" {
|
|
||||||
addr = ":1965"
|
|
||||||
}
|
|
||||||
certs := make([]tls.Certificate, len(configs))
|
|
||||||
for i, c := range configs {
|
|
||||||
cert, err := tls.LoadX509KeyPair(c.CertFile, c.KeyFile)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error loading certs: %s", err)
|
|
||||||
}
|
|
||||||
certs[i] = cert
|
|
||||||
}
|
|
||||||
|
|
||||||
config := tls.Config{Certificates: certs}
|
|
||||||
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 (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
|
|
||||||
}
|
|
||||||
var res Response
|
|
||||||
var req string
|
|
||||||
if !strings.Contains(string(data), "\r\n") {
|
|
||||||
res = Response{STATUS_BAD_REQUEST, "Request too large", ""}
|
|
||||||
req = "TOO_LONG_REQUEST"
|
|
||||||
} else if !utf8.Valid(data) {
|
|
||||||
res = Response{STATUS_BAD_REQUEST, "URL contains non UTF8 charcaters", ""}
|
|
||||||
} else {
|
|
||||||
req = string(data[:count-2])
|
|
||||||
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, c *conn) 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 s.HostnameToRoot[u.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.HostnameToRoot[u.Hostname()] + 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 isNotWorldReadable(fi):
|
|
||||||
return Response{STATUS_TEMPORARY_FAILURE, "Unable to access file", ""}
|
|
||||||
case fi.IsDir():
|
|
||||||
if strings.HasSuffix(u.Path, "/") {
|
|
||||||
return generateDirectory(selector)
|
|
||||||
} else {
|
|
||||||
return Response{STATUS_REDIRECT_PERMANENT, "gemini://" + u.Hostname() + u.Path + "/", ""}
|
|
||||||
}
|
|
||||||
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 (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
|
|
||||||
}
|
|
||||||
|
|
||||||
func isNotWorldReadable(file os.FileInfo) bool {
|
|
||||||
return uint64(file.Mode().Perm())&0444 != 0444
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user