# README
Gig - Gemini framework
API is subject to change until v1.0
Protocol compatibility
Version | Supported Gemini version |
---|---|
0.9.4 | v0.14.* |
< 0.9.4 | v0.13.* |
Contents
- Feature Overview
- Guide
- Quick Start
- Parameters in path
- Query
- Client Certificate
- Grouping routes
- Blank Gig without middleware by default
- Using middleware
- Writing logs to file
- Custom Log Format
- Serving static files
- Serving data from file
- Serving data from reader
- Templates
- Redirects
- Subdomains
- Username/password authentication middleware
- Custom middleware
- Custom port
- Custom TLS config
- Testing
- Contribute
- License
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.