Categorygithub.com/pitr/gig
modulepackage
0.9.8
Repository: https://github.com/pitr/gig.git
Documentation: pkg.go.dev

# README

Gig - Gemini framework

Sourcegraph godocs.io GoDoc Go Report Card Codecov License

API is subject to change until v1.0

Protocol compatibility

VersionSupported Gemini version
0.9.4v0.14.*
< 0.9.4v0.13.*

Contents

Feature Overview

  • Client certificate suppport (access x509.Certificate directly from context)
  • Highly optimized router with zero dynamic memory allocation which smartly prioritizes routes
  • Group APIs
  • Extensible middleware framework
  • Define middleware at root, group or route level
  • Handy functions to send variety of Gemini responses
  • Centralized error handling
  • Template rendering with any template engine
  • Define your format for the logger
  • Highly customizable

Guide

Quick Start

package main

import "github.com/pitr/gig"

func main() {
  // Gig instance
  g := gig.Default()

  // Routes
  g.Handle("/", func(c gig.Context) error {
    return c.Gemini("# Hello, World!")
  })

  // Start server on PORT or default port
  g.Run("my.crt", "my.key")
}
$ go run main.go

Parameters in path

package main

import "github.com/pitr/gig"

func main() {
  g := gig.Default()

  g.Handle("/user/:name", func(c gig.Context) error {
    return c.Gemini("# Hello, %s!", c.Param("name"))
  })

  g.Run("my.crt", "my.key")
}

Query

package main

import "github.com/pitr/gig"

func main() {
  g := gig.Default()

  g.Handle("/user", func(c gig.Context) error {
    query, err := c.QueryString()
    if err != nil {
      return err
    }
    return c.Gemini("# Hello, %s!", query)
  })

  g.Run("my.crt", "my.key")
}

Client Certificate

package main

import "github.com/pitr/gig"

func main() {
  g := gig.Default()

  g.Handle("/user", func(c gig.Context) error {
    cert := c.Certificate()
    if cert == nil {
      return NewError(gig.StatusClientCertificateRequired, "We need a certificate")
    }
    return c.Gemini("# Hello, %s!", cert.Subject.CommonName)
  })

  // OR
  g.Handle("/user", func(c gig.Context) error {
    return c.Gemini("# Hello, %s!", c.Get("subject"))
  }, gig.CertAuth(gig.ValidateHasCertificate))

  g.Run("my.crt", "my.key")
}

Grouping routes

func main() {
  g := gig.Default()

  // Simple group: v1
  v1 := g.Group("/v1")
  {
    v1.Handle("/page1", page1Endpoint)
    v1.Handle("/page2", page2Endpoint)
  }

  // Simple group: v2
  v2 := g.Group("/v2")
  {
    v2.Handle("/page1", page1Endpoint)
    v2.Handle("/page2", page2Endpoint)
  }

  g.Run("my.crt", "my.key")
}

Blank Gig without middleware by default

Use

g := gig.New()

instead of

// Default With the Logger and Recovery middleware already attached
g := gig.Default()

Using middleware

func main() {
  // Creates a router without any middleware by default
  g := gig.New()

  // Global middleware
  // Logger middleware will write the logs to gig.DefaultWriter.
  // By default gig.DefaultWriter = os.Stdout
  g.Use(gig.Logger())

  // Recovery middleware recovers from any panics and return StatusPermanentFailure.
  g.Use(gig.Recovery())

  // Private group
  // same as private := g.Group("/private", gig.CertAuth(gig.ValidateHasCertificate))
  private := g.Group("/private")
  private.Use(gig.CertAuth(gig.ValidateHasCertificate))
  {
    private.Handle("/user", userEndpoint)
  }

  g.Run("my.crt", "my.key")
}

Writing logs to file

func main() {
  f, _ := os.Create("access.log")
  gig.DefaultWriter = io.MultiWriter(f)

  // Use the following code if you need to write the logs to file and console at the same time.
  // gig.DefaultWriter = io.MultiWriter(f, os.Stdout)

  g := gig.Default()

  g.Handle("/", func(c gig.Context) error {
      return c.Gemini("# Hello, World!")
  })

  g.Run("my.crt", "my.key")
}

Custom Log Format

func main() {
  g := gig.New()

  // See LoggerConfig documentation for more
  g.Use(gig.LoggerWithConfig(gig.LoggerConfig{Format: "${remote_ip} ${status}"}))

  g.Handle("/", func(c gig.Context) error {
      return c.Gemini("# Hello, World!")
  })

  g.Run("my.crt", "my.key")
}

Serving static files

func main() {
  g := gig.Default()

  g.Static("/images", "images")
  g.Static("/robots.txt", "files/robots.txt")

  g.Run("my.crt", "my.key")
}

Serving data from file

func main() {
  g := gig.Default()

  g.Handle("/robots.txt", func(c gig.Context) error {
      return c.File("robots.txt")
  })

  g.Run("my.crt", "my.key")
}

Serving data from reader

func main() {
  g := gig.Default()

  g.Handle("/data", func(c gig.Context) error {
    response, err := http.Get("https://google.com/")
    if err != nil || response.StatusCode != http.StatusOK {
      return gig.ErrProxyError
    }

    return c.Stream("text/html", response.Body)
  })

  g.Run("my.crt", "my.key")
}

Templates

Use text/template, https://github.com/valyala/quicktemplate, or anything else. This example uses text/template

import (
  "text/template"

  "github.com/pitr/gig"
)

type Template struct {
    templates *template.Template
}

func (t *Template) Render(w io.Writer, name string, data interface{}, c gig.Context) error {
    return t.templates.ExecuteTemplate(w, name, data)
}

func main() {
  g := gig.Default()

  // Register renderer
  g.Renderer = &Template{
    templates: template.Must(template.ParseGlob("public/views/*.gmi")),
  }

  g.Handle("/user/:name", func(c gig.Context) error {
    return c.Render("user", c.Param("name"))
  })

  g.Run("my.crt", "my.key")
}

Consider bundling assets with the binary by using go-assets or similar.

Redirects

func main() {
  g := gig.Default()

  g.Handle("/old", func(c gig.Context) error {
    return c.NoContent(gig.StatusRedirectPermanent, "/new")
  })

  g.Run("my.crt", "my.key")
}

Subdomains

func main() {
  apps := map[string]*gig.Gig{}

  // App A
  a := gig.Default()
  apps["app-a.example.com"] = a

  a.Handle("/", func(c gig.Context) error {
      return c.Gemini("I am App A")
  })

  // App B
  b := gig.Default()
  apps["app-b.example.com"] = b

  b.Handle("/", func(c gig.Context) error {
      return c.Gemini("I am App B")
  })

  // Server
  g := gig.New()
  g.Handle("/*", func(c gig.Context) error {
      app := apps[c.URL().Host]

      if app == nil {
          return gig.ErrNotFound
      }

      app.ServeGemini(c)
      return nil
  })

  g.Run("my.crt", "my.key") // must be wildcard SSL certificate for *.example.com
}

Username/password authentication middleware

This assumes that there is a db module that does user management. This middleware ensures that there is a client certificate and validates its fingerprint using passed function. If authentication is required, user is redirected to path returned by middleware. Login handlers are setup using PassAuthLoginHandle function, which collectss username and password and pass to provided function. That function should return an error if login failed, or absolute path to redirect user to.

func main() {
  g := Default()

  secret := g.Group("/secret", gig.PassAuth(func(sig string, c gig.Context) (string, error) {
    ok, err := db.CheckValid(sig)
    if err != nil {
      return "/login", err
    }
    if !ok {
      return "/login", nil
    }
    return "", nil
  }))
  // secret.Handle("/page", func(c gig.Context) {...})

  g.PassAuthLoginHandle("/login", func(user, pass, sig string, c Context) (string, error) {
    // check user/pass combo, and activate cert signature if valid
    err := db.Login(user, pass, sig)
    if err != nil {
      return "", err
    }
    return "/secret/page", nil
  })

  g.Run("my.crt", "my.key")
}

Custom middleware

func MyMiddleware(next gig.HandlerFunc) gig.HandlerFunc {
  return func(c gig.Context) error {
    // Set example variable
    c.Set("example", "123")

    if err := next(c); err != nil {
      c.Error(err)
    }

    // Do something after request is done
    // ...

    return err
  }
}

func main() {
  g := gig.Default()
  g.Use(MyMiddleware)

  g.Handle("/", func(c gig.Context) error {
    return c.Gemini("# Example %s", c.Get("example"))
  })

  g.Run("my.crt", "my.key")
}

Custom port

Use PORT environment variable:

PORT=12345 ./myapp

Alternatively, pass it to Run:

func main() {
  g := gig.Default()

  g.Handle("/", func(c gig.Context) error {
    return c.Gemini("# Hello world")
  })

  g.Run(":12345", "my.crt", "my.key")
}

Custom TLS config

func main() {
  g := gig.Default()
  g.TLSConfig.MinVersion = tls.VersionTLS13

  g.Handle("/", func(c gig.Context) error {
    return c.Gemini("# Hello world")
  })

  g.Run("my.crt", "my.key")
}

Testing

func setupServer() *gig.Gig {
  g := gig.Default()

  g.Handle("/private", func(c gig.Context) error {
    return c.Gemini("Hello %s", c.Get("subject"))
  }, gig.CertAuth(gig.ValidateHasCertificate))

  return g
}

func TestServer(t *testing.T) {
  g := setupServer()
  c, res := g.NewFakeContext("/private", nil)

  g.ServeGemini(c)

  if res.Written != "60 Client Certificate Required\r\n" {
    t.Fail()
  }
}

func TestCertificate(t *testing.T) {
  g := setupServer()
  c, res := g.NewFakeContext("/", &tls.ConnectionState{
    PeerCertificates: []*x509.Certificate{
      {Subject: pkix.Name{CommonName: "john"}},
    },
  })

  g.ServeGemini(c)

  if resp.Written != "20 text/gemini\r\nHello john" {
    t.Fail()
  }
}

Contribute

If something is missing, please open an issue. If possible, send a PR.

License

MIT

# Packages

No description provided by the author

# Functions

CertAuth returns an CertAuth middleware.
CertAuthWithConfig returns an CertAuth middleware with config.
Default returns a Gig instance with Logger and Recover middleware enabled.
DefaultGeminiErrorHandler is the default HTTP error handler.
DefaultSkipper returns false which processes the middleware.
Logger returns a middleware that logs Gemini requests.
LoggerWithConfig returns a Logger middleware with config.
New creates an instance of Gig.
NewError creates a new GeminiError instance.
NewErrorFrom creates a new GeminiError instance using Code from existing GeminiError.
NewResponse creates a new instance of Response.
PassAuth is a middleware that implements username/password authentication by first requiring a certificate, checking username/password using PassAuthValidator, and then pinning certificate to it.
Recover returns a middleware which recovers from panics anywhere in the chain and handles the control to the centralized GeminiErrorHandler.
RecoverWithConfig returns a Recover middleware with config.
ValidateHasCertificate returns ErrClientCertificateRequired if no certificate is sent.

# Constants

MIME types.
MIME types.
MIME types.
MIME types.
Gemini status codes as documented by specification.
Gemini status codes as documented by specification.
Gemini status codes as documented by specification.
Gemini status codes as documented by specification.
Gemini status codes as documented by specification.
Gemini status codes as documented by specification.
Gemini status codes as documented by specification.
Gemini status codes as documented by specification.
Gemini status codes as documented by specification.
Gemini status codes as documented by specification.
Gemini status codes as documented by specification.
Gemini status codes as documented by specification.
Gemini status codes as documented by specification.
Gemini status codes as documented by specification.
Gemini status codes as documented by specification.
Gemini status codes as documented by specification.
Gemini status codes as documented by specification.
Gemini status codes as documented by specification.
Version of Gig.

# Variables

Debug enables gig to print its internal debug messages.
DefaultCertAuthConfig is the default CertAuth middleware config.
DefaultLoggerConfig is the default Logger middleware config.
DefaultRecoverConfig is the default Recover middleware config.
DefaultWriter is the default io.Writer used by gig for debug output and middleware output like Logger() or Recovery().
Errors that can be inherited from using NewErrorFrom.
Errors that can be inherited from using NewErrorFrom.
Errors that can be inherited from using NewErrorFrom.
Errors that can be inherited from using NewErrorFrom.
Errors that can be inherited from using NewErrorFrom.
Errors that can be inherited from using NewErrorFrom.
Errors that can be inherited from using NewErrorFrom.
Errors that can be inherited from using NewErrorFrom.
Errors that can be inherited from using NewErrorFrom.
Errors that can be inherited from using NewErrorFrom.
Errors that can be inherited from using NewErrorFrom.
Errors that can be inherited from using NewErrorFrom.
Errors that can be inherited from using NewErrorFrom.
Errors that can be inherited from using NewErrorFrom.
Errors that can be inherited from using NewErrorFrom.
Errors that can be inherited from using NewErrorFrom.
Error handlers.

# Structs

No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author

# Interfaces

No description provided by the author
No description provided by the author

# Type aliases

No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
Status is a Gemini status code type.