package gig

import (

"crypto/md5"

"crypto/tls"

"crypto/x509"

"fmt"

"io"

"io/ioutil"

"mime"

"net"

"net/url"

"os"

"path"

"path/filepath"

"sort"

"strings"

"sync"

)

type (

// Context represents the context of the current request. It holds connection

// reference, path, path parameters, data and registered handler.

// DO NOT retain Context instance, as it will be reused by other connections.

Context interface {

// Response returns `*Response`.

Response() *Response

// IP returns the client's network address.

IP() string

// Certificate returns client's leaf certificate or nil if none provided

Certificate() *x509.Certificate

// CertHash returns a hash of client's leaf certificate or empty string is none

CertHash() string

// URL returns the URL for the context.

URL() *url.URL

// Path returns the registered path for the handler.

Path() string

// QueryString returns unescaped URL query string or error if the raw query

// could not be unescaped. Use Context#URL().RawQuery to get raw query string.

QueryString() (string, error)

// RequestURI is the unmodified URL string as sent by the client

// to a server. Usually the URL() or Path() should be used instead.

RequestURI() string

// Param returns path parameter by name.

Param(name string) string

// Get retrieves data from the context.

Get(key string) interface{}

// Set saves data in the context.

Set(key string, val interface{})

// Render renders a template with data and sends a text/gemini response with status

// code Success. Renderer must be registered using `Gig.Renderer`.

Render(name string, data interface{}) error

// Gemini sends a text/gemini response with status code Success.

Gemini(text string, args ...interface{}) error

// GeminiBlob sends a text/gemini blob response with status code Success.

GeminiBlob(b []byte) error

// Text sends a text/plain response with status code Success.

Text(format string, values ...interface{}) error

// Blob sends a blob response with status code Success and content type.

Blob(contentType string, b []byte) error

// Stream sends a streaming response with status code Success and content type.

Stream(contentType string, r io.Reader) error

// File sends a response with the content of the file.

File(file string) error

// NoContent sends a response with no body, and a status code and meta field.

// Use for any non-2x status codes

NoContent(code Status, meta string, values ...interface{}) error

// Error invokes the registered error handler. Generally used by middleware.

Error(err error)

// Handler returns the matched handler by router.

Handler() HandlerFunc

// Gig returns the `Gig` instance.

Gig() *Gig

}

context struct {

conn tlsconn

TLS *tls.ConnectionState

u *url.URL

response *Response

path string

requestURI string

pnames []string

pvalues []string

handler HandlerFunc

store storeMap

gig *Gig

lock sync.RWMutex

}

)

const (

indexPage = "index.gmi"

)

func (c *context) Response() *Response {

return c.response

}

func (c *context) IP() string {

ra, _, _ := net.SplitHostPort(c.conn.RemoteAddr().String())

return ra

}

func (c *context) Certificate() *x509.Certificate {

if c.TLS == nil || len(c.TLS.PeerCertificates) == 0 {

return nil

}

return c.TLS.PeerCertificates[0]

}

func (c *context) CertHash() string {

cert := c.Certificate()

if cert == nil {

return ""

}

return fmt.Sprintf("%x", md5.Sum(cert.Raw))

}

func (c *context) URL() *url.URL {

return c.u

}

func (c *context) Path() string {

return c.path

}

func (c *context) RequestURI() string {

return c.requestURI

}

func (c *context) Param(name string) string {

for i, n := range c.pnames {

if i < len(c.pvalues) {

if n == name {

return c.pvalues[i]

}

}

}

return ""

}

func (c *context) QueryString() (string, error) {

return url.QueryUnescape(c.u.RawQuery)

}

func (c *context) Get(key string) interface{} {

c.lock.RLock()

defer c.lock.RUnlock()

return c.store[key]

}

func (c *context) Set(key string, val interface{}) {

c.lock.Lock()

defer c.lock.Unlock()

if c.store == nil {

c.store = make(storeMap)

}

c.store[key] = val

}

func (c *context) Render(name string, data interface{}) (err error) {

if c.gig.Renderer == nil {

return ErrRendererNotRegistered

}

if err = c.response.WriteHeader(StatusSuccess, MIMETextGemini); err != nil {

return

}

return c.gig.Renderer.Render(c.response, name, data, c)

}

func (c *context) Gemini(format string, values ...interface{}) error {

return c.GeminiBlob([]byte(fmt.Sprintf(format, values...)))

}

func (c *context) GeminiBlob(b []byte) (err error) {

return c.Blob(MIMETextGemini, b)

}

func (c *context) Text(format string, values ...interface{}) (err error) {

return c.Blob(MIMETextPlain, []byte(fmt.Sprintf(format, values...)))

}

func (c *context) Blob(contentType string, b []byte) (err error) {

err = c.response.WriteHeader(StatusSuccess, contentType)

if err != nil {

return

}

_, err = c.response.Write(b)

return

}

func (c *context) Stream(contentType string, r io.Reader) (err error) {

err = c.response.WriteHeader(StatusSuccess, contentType)

if err != nil {

return

}

_, err = io.Copy(c.response, r)

return

}

func (c *context) File(file string) (err error) {

if containsDotDot(file) {

c.Error(ErrBadRequest)

return

}

s, err := os.Stat(file)

if err != nil {

c.Error(ErrNotFound)

return

}

if uint64(s.Mode().Perm())&0444 != 0444 {

c.Error(ErrGone)

return

}

if s.IsDir() {

files, err := ioutil.ReadDir(file)

if err != nil {

c.Error(ErrTemporaryFailure)

return err

}

for _, f := range files {

if f.Name() == indexPage {

return c.File(path.Join(file, indexPage))

}

}

err = c.response.WriteHeader(StatusSuccess, "text/gemini")

if err != nil {

return err

}

_, _ = c.response.Write([]byte(fmt.Sprintf("# Listing %s\n\n", c.u.Path)))

sort.Slice(files, func(i, j int) bool { return files[i].Name() < files[j].Name() })

for _, file := range files {

if strings.HasPrefix(file.Name(), ".") {

continue

}

if uint64(file.Mode().Perm())&0444 != 0444 {

continue

}

_, _ = c.response.Write([]byte(fmt.Sprintf("=> %s %s [ %v ]\n", filepath.Clean(path.Join(c.u.Path, file.Name())), file.Name(), bytefmt(file.Size()))))

}

return nil

}

ext := filepath.Ext(file)

var mimeType string

if ext == ".gmi" {

mimeType = "text/gemini"

} else {

mimeType = mime.TypeByExtension(ext)

if mimeType == "" {

mimeType = "octet/stream"

}

}

f, err := os.OpenFile(file, os.O_RDONLY, 0600)

if err != nil {

c.Error(ErrTemporaryFailure)

return

}

defer f.Close()

err = c.response.WriteHeader(StatusSuccess, mimeType)

if err != nil {

return

}

_, err = io.Copy(c.response, f)

if err != nil {

// .. remote closed the connection, nothing we can do besides log

// or io error, but status is already sent, everything is broken!

c.Error(ErrTemporaryFailure)

}

return

}

func containsDotDot(v string) bool {

if !strings.Contains(v, "..") {

return false

}

for _, ent := range strings.FieldsFunc(v, isSlashRune) {

if ent == ".." {

return true

}

}

return false

}

func isSlashRune(r rune) bool { return r == '/' || r == '\\' }

func (c *context) NoContent(code Status, meta string, values ...interface{}) error {

return c.response.WriteHeader(code, fmt.Sprintf(meta, values...))

}

func (c *context) Error(err error) {

c.gig.GeminiErrorHandler(err, c)

}

func (c *context) Gig() *Gig {

return c.gig

}

func (c *context) Handler() HandlerFunc {

return c.handler

}

func (c *context) reset(conn tlsconn, u *url.URL, requestURI string, tls *tls.ConnectionState) {

c.conn = conn

c.TLS = tls

c.u = u

c.requestURI = requestURI

c.response.reset(conn)

c.handler = NotFoundHandler

c.store = nil

c.path = ""

c.pnames = nil

// NOTE: Don't reset because it has to have length c.gig.maxParam at all times

for i := 0; i < *c.gig.maxParam; i++ {

c.pvalues[i] = ""

}

}

func bytefmt(b int64) string {

const unit = 1000

if b < unit {

return fmt.Sprintf("%dB", b)

}

div, exp := int64(unit), 0

for n := b / unit; n >= unit; n /= unit {

div *= unit

exp++

}

return fmt.Sprintf("%.1f%cB", float64(b)/float64(div), "kMGTPE"[exp])

}


Source