diff --git a/flake.nix b/flake.nix index 3107f8c..c5f74f0 100644 --- a/flake.nix +++ b/flake.nix @@ -17,7 +17,7 @@ version = "dev"; src = ./src; - vendorSha256 = "sha256:1vazrrg8rs9n8x40c9r53h9qnyxw59xkp0aq7jl15fliigk6q0cr"; + vendorSha256 = "sha256-02LW4zscNKoIfzcBhOQwObh/04oRl/6hRsFMfCycWzA="; subPackages = [ "cmd/mediocre-blog" ]; @@ -26,7 +26,7 @@ }; devShell = pkgs.mkShell { - buildInputs = [ pkgs.go pkgs.sqlite ]; + buildInputs = [ pkgs.go pkgs.sqlite pkgs.amfora ]; shellHook = '' export MEDIOCRE_BLOG_DATA_DIR="/tmp/mediocre-blog/data" @@ -49,13 +49,19 @@ export MEDIOCRE_BLOG_HTTP_AUTH_USERS='{"foo":"$2a$13$0JdWlUfHc.3XimEMpEu1cuu6RodhUvzD9l7iiAqa4YkM3mcFV5Pxi"}' export MEDIOCRE_BLOG_HTTP_AUTH_RATELIMIT="1s" + # gmi + export MEDIOCRE_BLOG_GEMINI_PUBLIC_URL="gemini://localhost:2096" + export MEDIOCRE_BLOG_GEMINI_LISTEN_ADDR=":2065" + export MEDIOCRE_BLOG_GEMINI_CERTIFICATES_PATH="$MEDIOCRE_BLOG_DATA_DIR/gmi/certs" + cd src echo 'Loading test data...' (cd cmd/load-test-data && go run main.go) echo -e "\n\nTest data has been loaded into $MEDIOCRE_BLOG_DATA_DIR\n" - echo -e "You can do 'go run ./cmd/mediocre-blog/main.go' to start a dev instance on http://localhost:4000\n\n" + echo -e "You can do 'go run ./cmd/mediocre-blog/main.go' to start a dev instance on http://localhost:4000\n" + echo -e "You can then do 'amfora gemini://localhost:2065' to test the gemini server\n" ''; }; diff --git a/src/cmd/mediocre-blog/main.go b/src/cmd/mediocre-blog/main.go index 6b41e04..8c6939f 100644 --- a/src/cmd/mediocre-blog/main.go +++ b/src/cmd/mediocre-blog/main.go @@ -8,6 +8,7 @@ import ( "time" cfgpkg "github.com/mediocregopher/blog.mediocregopher.com/srv/cfg" + "github.com/mediocregopher/blog.mediocregopher.com/srv/gmi" "github.com/mediocregopher/blog.mediocregopher.com/srv/http" "github.com/mediocregopher/blog.mediocregopher.com/srv/mailinglist" "github.com/mediocregopher/blog.mediocregopher.com/srv/post" @@ -44,6 +45,10 @@ func main() { httpParams.SetupCfg(cfg) ctx = mctx.WithAnnotator(ctx, &httpParams) + var gmiParams gmi.Params + gmiParams.SetupCfg(cfg) + ctx = mctx.WithAnnotator(ctx, &gmiParams) + // initialization err := cfg.Init(ctx) @@ -104,10 +109,10 @@ func main() { httpParams.PostDraftStore = postDraftStore httpParams.MailingList = ml - logger.Info(ctx, "listening") + logger.Info(ctx, "starting http api") httpAPI, err := http.New(httpParams) if err != nil { - logger.Fatal(ctx, "initializing http api", err) + logger.Fatal(ctx, "starting http api", err) } defer func() { shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second) @@ -118,6 +123,23 @@ func main() { } }() + gmiParams.Logger = logger.WithNamespace("gmi") + + logger.Info(ctx, "starting gmi api") + gmiAPI, err := gmi.New(gmiParams) + if err != nil { + logger.Fatal(ctx, "starting gmi api", err) + } + + defer func() { + shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + if err := gmiAPI.Shutdown(shutdownCtx); err != nil { + logger.Fatal(ctx, "shutting down gmi api", err) + } + }() + // wait sigCh := make(chan os.Signal, 1) diff --git a/src/gmi/gmi.go b/src/gmi/gmi.go index 358d935..6e2d79f 100644 --- a/src/gmi/gmi.go +++ b/src/gmi/gmi.go @@ -1,2 +1,122 @@ -// Package gmi contains utilities for working with gemini and gemtext +// Package gmi implements the gemini-based api for the mediocre-blog. package gmi + +import ( + "context" + "errors" + "fmt" + "net/url" + "os" + "strings" + + "git.sr.ht/~adnano/go-gemini" + "git.sr.ht/~adnano/go-gemini/certificate" + "github.com/mediocregopher/blog.mediocregopher.com/srv/cfg" + "github.com/mediocregopher/mediocre-go-lib/v2/mctx" + "github.com/mediocregopher/mediocre-go-lib/v2/mlog" +) + +// Params are used to instantiate a new API instance. All fields are required +// unless otherwise noted. +type Params struct { + Logger *mlog.Logger + PublicURL *url.URL + ListenAddr string + CertificatesPath string +} + +// SetupCfg implement the cfg.Cfger interface. +func (p *Params) SetupCfg(cfg *cfg.Cfg) { + + publicURLStr := cfg.String("gemini-public-url", "gemini://localhost:2065", "URL this service is accessible at") + + cfg.StringVar(&p.ListenAddr, "gemini-listen-addr", ":2065", "Address to listen for HTTP requests on") + + cfg.StringVar(&p.CertificatesPath, "gemini-certificates-path", "", "Path to directory where gemini certs should be created/stored") + + cfg.OnInit(func(context.Context) error { + + if p.CertificatesPath == "" { + return errors.New("-gemini-certificates-path is required") + } + + var err error + + *publicURLStr = strings.TrimSuffix(*publicURLStr, "/") + + if p.PublicURL, err = url.Parse(*publicURLStr); err != nil { + return fmt.Errorf("parsing -gemini-public-url: %w", err) + } + + return nil + }) +} + +// Annotate implements mctx.Annotator interface. +func (p *Params) Annotate(a mctx.Annotations) { + a["geminiPublicURL"] = p.PublicURL + a["geminiListenAddr"] = p.ListenAddr + a["geminiCertificatesPath"] = p.CertificatesPath +} + +// API will listen on the port configured for it, and serve gemini requests for +// the mediocre-blog. +type API interface { + Shutdown(ctx context.Context) error +} + +type api struct { + params Params + srv *gemini.Server +} + +// New initializes and returns a new API instance, including setting up all +// listening ports. +func New(params Params) (API, error) { + + if err := os.MkdirAll(params.CertificatesPath, 0700); err != nil { + return nil, fmt.Errorf("creating certificate directory %q: %w", params.CertificatesPath, err) + } + + certStore := new(certificate.Store) + certStore.Load(params.CertificatesPath) + certStore.Register(params.PublicURL.Hostname()) + + a := &api{ + params: params, + } + + a.srv = &gemini.Server{ + Addr: params.ListenAddr, + Handler: a.handler(), + GetCertificate: certStore.Get, + } + + go func() { + + ctx := mctx.WithAnnotator(context.Background(), &a.params) + + err := a.srv.ListenAndServe(ctx) + if err != nil && !errors.Is(err, context.Canceled) { + a.params.Logger.Fatal(ctx, "serving gemini server", err) + } + }() + + return a, nil +} + +func (a *api) Shutdown(ctx context.Context) error { + return a.srv.Shutdown(ctx) +} + +func (a *api) handler() gemini.Handler { + return gemini.HandlerFunc(func( + ctx context.Context, + rw gemini.ResponseWriter, + r *gemini.Request, + ) { + fmt.Fprintf(rw, "# Test\n\n") + fmt.Fprintf(rw, "HELLO WORLD\n\n") + fmt.Fprintf(rw, "=> gemini://midnight.pub Hit the pub\n\n") + }) +} diff --git a/src/go.mod b/src/go.mod index 4b047b9..4f4cf70 100644 --- a/src/go.mod +++ b/src/go.mod @@ -3,6 +3,7 @@ module github.com/mediocregopher/blog.mediocregopher.com/srv go 1.16 require ( + git.sr.ht/~adnano/go-gemini v0.2.3 // indirect github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 github.com/emersion/go-smtp v0.15.0 github.com/gomarkdown/markdown v0.0.0-20220510115730-2372b9aa33e5 diff --git a/src/go.sum b/src/go.sum index eb27d7f..c26e7df 100644 --- a/src/go.sum +++ b/src/go.sum @@ -1,4 +1,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +git.sr.ht/~adnano/go-gemini v0.2.3 h1:oJ+Y0/mheZ4Vg0ABjtf5dlmvq1yoONStiaQvmWWkofc= +git.sr.ht/~adnano/go-gemini v0.2.3/go.mod h1:hQ75Y0i5jSFL+FQ7AzWVAYr5LQsaFC7v3ZviNyj46dY= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= @@ -193,6 +195,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -216,6 +220,7 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/src/http/http.go b/src/http/http.go index e5ca3f1..da39374 100644 --- a/src/http/http.go +++ b/src/http/http.go @@ -64,7 +64,7 @@ func (p *Params) SetupCfg(cfg *cfg.Cfg) { publicURLStr := cfg.String("http-public-url", "http://localhost:4000", "URL this service is accessible at") cfg.StringVar(&p.ListenProto, "http-listen-proto", "tcp", "Protocol to listen for HTTP requests with") - cfg.StringVar(&p.ListenAddr, "http-listen-addr", ":4000", "Address/path to listen for HTTP requests on") + cfg.StringVar(&p.ListenAddr, "http-listen-addr", ":4000", "Address/unix socket path to listen for HTTP requests on") httpAuthUsersStr := cfg.String("http-auth-users", "{}", "JSON object with usernames as values and password hashes (produced by the hash-password binary) as values. Denotes users which are able to edit server-side data") @@ -141,7 +141,7 @@ func New(params Params) (API, error) { err := a.srv.Serve(l) if err != nil && !errors.Is(err, http.ErrServerClosed) { - ctx := mctx.Annotate(context.Background(), a.params) + ctx := mctx.WithAnnotator(context.Background(), &a.params) params.Logger.Fatal(ctx, "serving http server", err) } }()