initial release

main
Eric Bower 5 months ago
commit 8d86bdd33d
No known key found for this signature in database
GPG Key ID: 7A51A47D76D3FD02
  1. 14
      .gitignore
  2. 8
      Caddyfile
  3. 21
      Dockerfile
  4. 8
      Dockerfile.caddy
  5. 21
      LICENSE
  6. 76
      Makefile
  7. 108
      README.md
  8. 0
      build/.gitkeep
  9. 94
      cmd/ssh/main.go
  10. 7
      cmd/web/main.go
  11. 40
      db/migrations/20220310_init.sql
  12. 8
      db/migrations/20220422_add_desc_to_user_and_post.sql
  13. 2
      db/migrations/20220426_add_index_for_filename.sql
  14. 1
      db/migrations/20220427_username_to_lower.sql
  15. 5
      db/migrations/20220523_timestamp_with_tz.sql
  16. 1
      db/setup.sql
  17. 3
      db/teardown.sql
  18. 9
      docker-compose.yml
  19. 46
      go.mod
  20. 160
      go.sum
  21. 18
      html/base.layout.tmpl
  22. 44
      html/blog.page.tmpl
  23. 6
      html/footer.partial.tmpl
  24. 108
      html/help.page.tmpl
  25. 12
      html/marketing-footer.partial.tmpl
  26. 99
      html/marketing.page.tmpl
  27. 124
      html/ops.page.tmpl
  28. 36
      html/post.page.tmpl
  29. 52
      html/privacy.page.tmpl
  30. 35
      html/read.page.tmpl
  31. 1
      html/rss.page.tmpl
  32. 57
      html/transparency.page.tmpl
  33. 499
      internal/api.go
  34. 116
      internal/config.go
  35. 119
      internal/db_handler.go
  36. 32
      internal/parser.go
  37. 123
      internal/router.go
  38. 107
      internal/util.go
  39. 49
      production.yml
  40. BIN
      public/apple-touch-icon.png
  41. BIN
      public/card.png
  42. BIN
      public/favicon-16x16.png
  43. BIN
      public/favicon.ico
  44. 287
      public/main.css
  45. 2
      public/robots.txt

14
.gitignore vendored

@ -0,0 +1,14 @@
*.log
*.swp
.env
.envrc
build/*
!build/.gitkeep
ssh_data/*
!ssh_data/.gitkeep
caddy_data/*
!caddy_data/.gitkeep
caddy_config/*
!caddy_config/.gitkeep
.env.prod
*.bak

@ -0,0 +1,8 @@
*.pastes.sh, pastes.sh {
reverse_proxy web:3000
tls hello@pastes.sh
tls {
dns cloudflare {env.CF_API_TOKEN}
}
encode zstd gzip
}

@ -0,0 +1,21 @@
FROM golang:1.18.1-alpine3.15 AS builder
RUN apk add --no-cache git
WORKDIR /app
COPY . ./
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o ./build/ssh ./cmd/ssh
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o ./build/web ./cmd/web
FROM alpine:3.15 AS ssh
WORKDIR /app
COPY --from=0 /app/build/ssh ./
CMD ["./ssh"]
FROM alpine:3.15 AS web
WORKDIR /app
COPY --from=0 /app/build/web ./
COPY --from=0 /app/html ./html
COPY --from=0 /app/public ./public
CMD ["./web"]

@ -0,0 +1,8 @@
FROM caddy:builder-alpine AS builder
RUN xcaddy build \
--with github.com/caddy-dns/cloudflare
FROM caddy:alpine
COPY --from=builder /usr/bin/caddy /usr/bin/caddy

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Eric Bower
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,76 @@
PGDATABASE?="pastes"
PGHOST?="db"
PGUSER?="postgres"
PORT?="5432"
DB_CONTAINER?=pastessh_db_1
test:
docker run --rm -v $(shell pwd):/app -w /app golangci/golangci-lint:latest golangci-lint run -E goimports -E godot
.PHONY: test
build:
go build -o build/web ./cmd/web
go build -o build/ssh ./cmd/ssh
.PHONY: build
format:
go fmt ./...
.PHONY: format
create:
docker exec -i $(DB_CONTAINER) psql -U $(PGUSER) < ./db/setup.sql
.PHONY: create
teardown:
docker exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./db/teardown.sql
.PHONY: teardown
migrate:
docker exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./db/migrations/20220310_init.sql
docker exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./db/migrations/20220422_add_desc_to_user_and_post.sql
docker exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./db/migrations/20220426_add_index_for_filename.sql
docker exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./db/migrations/20220427_username_to_lower.sql
docker exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./db/migrations/20220523_timestamp_with_tz.sql
.PHONY: migrate
latest:
docker exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./db/migrations/20220523_timestamp_with_tz.sql
.PHONY: latest
psql:
docker exec -it $(DB_CONTAINER) psql -U $(PGUSER)
.PHONY: psql
dump:
docker exec -it $(DB_CONTAINER) pg_dump -U $(PGUSER) $(PGDATABASE) > ./backup.sql
.PHONY: dump
restore:
docker cp ./backup.sql $(DB_CONTAINER):/backup.sql
docker exec -it $(DB_CONTAINER) /bin/bash
# psql postgres -U postgres < /backup.sql
.PHONY: restore
bp-caddy:
docker build -t neurosnap/pastes-caddy -f Dockerfile.caddy .
docker push neurosnap/pastes-caddy
.PHONY: bp-caddy
bp-ssh:
docker build -t neurosnap/pastes-ssh --target ssh .
docker push neurosnap/pastes-ssh
.PHONY: bp-ssh
bp-web:
docker build -t neurosnap/pastes-web --target web .
docker push neurosnap/pastes-web
.PHONY: bp-web
bp: bp-ssh bp-web bp-caddy
.PHONY: bp
deploy:
docker system prune -f
docker-compose -f production.yml pull --ignore-pull-failures
docker-compose -f production.yml up --no-deps -d
.PHONY: deploy

@ -0,0 +1,108 @@
# pastes.sh
A pastebin service for hackers.
## comms
- [website](https://pico.sh)
- [irc #pico.sh](irc://irc.libera.chat/#pico.sh)
- [mailing list](https://lists.sr.ht/~erock/pico.sh)
- [ticket tracker](https://todo.sr.ht/~erock/pico.sh)
- [email](mailto:hello@pico.sh)
## setup
- golang `v1.18`
You'll also need some environment variables
```
export POSTGRES_PASSWORD="secret"
export DATABASE_URL="postgresql://postgres:secret@db/pastes?sslmode=disable"
export PASTES_SSH_PORT=2222
export PASTES_WEB_PORT=3000
export PASTES_DOMAIN="pastes.sh"
export PASTES_EMAIL="hello@pastes.sh"
```
I just use `direnv` which will load my `.env` file.
## development
### db
I use `docker-compose` to standup a postgresql server. If you already have a
server running you can skip this step.
Copy example `.env`
```bash
cp .env.example .env
```
Then run docker compose.
```bash
docker-compose up -d
```
Then create the database and migrate
```bash
make create
make migrate
```
### build the apps
```bash
make build
```
### run the apps
There are two apps: an ssh and web server.
```bash
./build/ssh
```
Default port for ssh server is `2222`.
```bash
./build/web
```
Default port for web server is `3000`.
### subdomains
Since we use subdomains for blogs, you'll need to update your `/etc/hosts` file
to accommodate.
```bash
# /etc/hosts
127.0.0.1 pastes.test
127.0.0.1 erock.pastes.test
```
Wildcards are not support in `/etc/hosts` so you'll have to add a subdomain for
each blog in development. For this example you'll also want to change the domain
env var to `PASTES_DOMAIN=pastes.test`.
## deployment
I use `docker-compose` for deployment. First you need `.env.prod`.
```bash
cp .env.example .env.prod
```
The `production.yml` file in this repo uses my docker hub images for deployment.
```bash
docker-compose -f production.yml up -d
```
If you want to deploy using your own domain then you'll need to edit the
`Caddyfile` with your domain.

@ -0,0 +1,94 @@
package main
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"time"
"git.sr.ht/~erock/pastes.sh/internal"
"git.sr.ht/~erock/wish/cms"
"git.sr.ht/~erock/wish/cms/db/postgres"
"git.sr.ht/~erock/wish/proxy"
"git.sr.ht/~erock/wish/send/scp"
"git.sr.ht/~erock/wish/send/sftp"
"github.com/charmbracelet/wish"
bm "github.com/charmbracelet/wish/bubbletea"
lm "github.com/charmbracelet/wish/logging"
"github.com/gliderlabs/ssh"
)
type SSHServer struct{}
func (me *SSHServer) authHandler(ctx ssh.Context, key ssh.PublicKey) bool {
return true
}
func createRouter(handler *internal.DbHandler) proxy.Router {
return func(sh ssh.Handler, s ssh.Session) []wish.Middleware {
cmd := s.Command()
mdw := []wish.Middleware{}
if len(cmd) == 0 {
mdw = append(mdw,
bm.Middleware(cms.Middleware(&handler.Cfg.ConfigCms, handler.Cfg)),
lm.Middleware(),
)
} else if cmd[0] == "scp" {
mdw = append(mdw, scp.Middleware(handler))
}
return mdw
}
}
func withProxy(handler *internal.DbHandler) ssh.Option {
return func(server *ssh.Server) error {
err := sftp.SSHOption(handler)(server)
if err != nil {
return err
}
return proxy.WithProxy(createRouter(handler))(server)
}
}
func main() {
host := internal.GetEnv("PASTES_HOST", "0.0.0.0")
port := internal.GetEnv("PASTES_SSH_PORT", "2222")
cfg := internal.NewConfigSite()
logger := cfg.Logger
dbh := postgres.NewDB(&cfg.ConfigCms)
defer dbh.Close()
handler := internal.NewDbHandler(dbh, cfg)
sshServer := &SSHServer{}
s, err := wish.NewServer(
wish.WithAddress(fmt.Sprintf("%s:%s", host, port)),
wish.WithHostKeyPath("ssh_data/term_info_ed25519"),
wish.WithPublicKeyAuth(sshServer.authHandler),
withProxy(handler),
)
if err != nil {
logger.Fatal(err)
}
done := make(chan os.Signal, 1)
signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
logger.Infof("Starting SSH server on %s:%s", host, port)
go func() {
if err = s.ListenAndServe(); err != nil {
logger.Fatal(err)
}
}()
<-done
logger.Info("Stopping SSH server")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer func() { cancel() }()
if err := s.Shutdown(ctx); err != nil {
logger.Fatal(err)
}
}

@ -0,0 +1,7 @@
package main
import "git.sr.ht/~erock/pastes.sh/internal"
func main() {
internal.StartApiServer()
}

@ -0,0 +1,40 @@
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE IF NOT EXISTS app_users (
id uuid NOT NULL DEFAULT uuid_generate_v4(),
name character varying(50),
created_at timestamp without time zone NOT NULL DEFAULT NOW(),
CONSTRAINT unique_name UNIQUE (name),
CONSTRAINT app_user_pkey PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS public_keys (
id uuid NOT NULL DEFAULT uuid_generate_v4(),
user_id uuid NOT NULL,
public_key varchar(2048) NOT NULL,
created_at timestamp without time zone NOT NULL DEFAULT NOW(),
CONSTRAINT user_public_keys_pkey PRIMARY KEY (id),
CONSTRAINT unique_key_for_user UNIQUE (user_id, public_key),
CONSTRAINT fk_user_public_keys_owner
FOREIGN KEY(user_id)
REFERENCES app_users(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
CREATE TABLE IF NOT EXISTS posts (
id uuid NOT NULL DEFAULT uuid_generate_v4(),
user_id uuid NOT NULL,
title character varying(255) NOT NULL,
text text NOT NULL DEFAULT '',
publish_at timestamp without time zone NOT NULL DEFAULT NOW(),
created_at timestamp without time zone NOT NULL DEFAULT NOW(),
updated_at timestamp without time zone NOT NULL DEFAULT NOW(),
CONSTRAINT posts_pkey PRIMARY KEY (id),
CONSTRAINT unique_title_for_user UNIQUE (user_id, title),
CONSTRAINT fk_posts_app_users
FOREIGN KEY(user_id)
REFERENCES app_users(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);

@ -0,0 +1,8 @@
ALTER TABLE app_users ADD COLUMN bio character varying(150) NOT NULL DEFAULT '';
ALTER TABLE posts ADD COLUMN description character varying(150) NOT NULL DEFAULT '';
ALTER TABLE posts ADD COLUMN filename character varying(255);
UPDATE posts SET filename = title;
ALTER TABLE posts ADD CONSTRAINT unique_filename_for_user UNIQUE (user_id, filename);
ALTER TABLE posts DROP CONSTRAINT unique_title_for_user;

@ -0,0 +1,2 @@
CREATE INDEX posts_filename ON posts USING btree(filename);
ALTER TABLE app_users DROP COLUMN bio;

@ -0,0 +1 @@
UPDATE app_users SET name = LOWER(name) WHERE name != LOWER(name);

@ -0,0 +1,5 @@
ALTER TABLE posts ALTER COLUMN updated_at TYPE timestamp WITH TIME ZONE USING updated_at AT TIME ZONE 'UTC';
ALTER TABLE posts ALTER COLUMN publish_at TYPE timestamp WITH TIME ZONE USING publish_at AT TIME ZONE 'UTC';
ALTER TABLE posts ALTER COLUMN created_at TYPE timestamp WITH TIME ZONE USING created_at AT TIME ZONE 'UTC';
ALTER TABLE app_users ALTER COLUMN created_at TYPE timestamp WITH TIME ZONE USING created_at AT TIME ZONE 'UTC';
ALTER TABLE public_keys ALTER COLUMN created_at TYPE timestamp WITH TIME ZONE USING created_at AT TIME ZONE 'UTC';

@ -0,0 +1 @@
CREATE DATABASE "pastes" OWNER "postgres";

@ -0,0 +1,3 @@
DROP TABLE posts CASCADE;
DROP TABLE app_users CASCADE;
DROP TABLE public_keys CASCADE;

@ -0,0 +1,9 @@
version: "3.4"
services:
db:
image: postgres
restart: always
ports:
- "5432:5432"
env_file:
- .env

@ -0,0 +1,46 @@
module git.sr.ht/~erock/pastes.sh
go 1.18
require (
git.sr.ht/~erock/wish v0.0.0-20220713141740-64595ee518ac
github.com/alecthomas/chroma v0.10.0
github.com/charmbracelet/wish v0.5.0
github.com/gliderlabs/ssh v0.3.4
github.com/yuin/goldmark v1.4.12
github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594
github.com/yuin/goldmark-meta v1.1.0
go.uber.org/zap v1.21.0
)
require (
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/caarlos0/sshmarshal v0.1.0 // indirect
github.com/charmbracelet/bubbles v0.12.0 // indirect
github.com/charmbracelet/bubbletea v0.22.0 // indirect
github.com/charmbracelet/keygen v0.3.0 // indirect
github.com/charmbracelet/lipgloss v0.5.0 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/lib/pq v1.10.6 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.12.0 // indirect
github.com/pkg/sftp v1.13.5 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d // indirect
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b // indirect
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

160
go.sum

@ -0,0 +1,160 @@
git.sr.ht/~erock/wish v0.0.0-20220707194507-66e938674d95 h1:q3G01ELBZt3EZEOiYWfwdiqZWtp6PKYueRHT2eeOlSU=
git.sr.ht/~erock/wish v0.0.0-20220707194507-66e938674d95/go.mod h1:QZKk7m9jc9iXah90daPGhQkSfNfxSVvpb6nfVeI+MM0=
git.sr.ht/~erock/wish v0.0.0-20220713141740-64595ee518ac h1:d+q9VPi+kaZpYpZOoXPSEx2KtZ3Gcmkz+x/lpb/V6bU=
git.sr.ht/~erock/wish v0.0.0-20220713141740-64595ee518ac/go.mod h1:QZKk7m9jc9iXah90daPGhQkSfNfxSVvpb6nfVeI+MM0=
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/caarlos0/sshmarshal v0.1.0 h1:zTCZrDORFfWh526Tsb7vCm3+Yg/SfW/Ub8aQDeosk0I=
github.com/caarlos0/sshmarshal v0.1.0/go.mod h1:7Pd/0mmq9x/JCzKauogNjSQEhivBclCQHfr9dlpDIyA=
github.com/charmbracelet/bubbles v0.12.0 h1:fxb9U9yI60Hek3tcPmMTFya5NhvPrqpkpyMaNngFh7A=
github.com/charmbracelet/bubbles v0.12.0/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc=
github.com/charmbracelet/bubbletea v0.21.0/go.mod h1:GgmJMec61d08zXsOhqRC/AiOx4K4pmz+VIcRIm1FKr4=
github.com/charmbracelet/bubbletea v0.22.0 h1:E1BTNSE3iIrq0G0X6TjGAmrQ32cGCbFDPcIuImikrUc=
github.com/charmbracelet/bubbletea v0.22.0/go.mod h1:aoVIwlNlr5wbCB26KhxfrqAn0bMp4YpJcoOelbxApjs=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/keygen v0.3.0 h1:mXpsQcH7DDlST5TddmXNXjS0L7ECk4/kLQYyBcsan2Y=
github.com/charmbracelet/keygen v0.3.0/go.mod h1:1ukgO8806O25lUZ5s0IrNur+RlwTBERlezdgW71F5rM=
github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8=
github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
github.com/charmbracelet/wish v0.5.0 h1:FkkdNBFqrLABR1ciNrAL2KCxoyWfKhXnIGZw6GfAtPg=
github.com/charmbracelet/wish v0.5.0/go.mod h1:5GAn5SrDSZ7cgKjnC+3kDmiIo7I6k4/AYiRzC4+tpCk=
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/gliderlabs/ssh v0.3.4 h1:+AXBtim7MTKaLVPgvE+3mhewYRawNLTd+jEEz/wExZw=
github.com/gliderlabs/ssh v0.3.4/go.mod h1:ZSS+CUoKHDrqVakTfTWUlKSr9MtMFkC4UvtQKD7O914=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs=
github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 h1:kMlmsLSbjkikxQJ1IPwaM+7LJ9ltFu/fi8CRzvSnQmA=
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/cancelreader v0.2.0/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/cancelreader v0.2.1/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
github.com/muesli/termenv v0.12.0 h1:KuQRUE3PgxRFWhq4gHvZtPSLCGDqM5q/cYr1pZ39ytc=
github.com/muesli/termenv v0.12.0/go.mod h1:WCCv32tusQ/EEZ5S8oUIIrC/nIuBcxCVqlN4Xfkv+7A=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go=
github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.5/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg=
github.com/yuin/goldmark v1.4.12 h1:6hffw6vALvEDqJ19dOJvJKOoAOKe4NDaTqvd2sktGN0=
github.com/yuin/goldmark v1.4.12/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594 h1:yHfZyN55+5dp1wG7wDKv8HQ044moxkyGq12KFFMFDxg=
github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594/go.mod h1:U9ihbh+1ZN7fR5Se3daSPoz1CGF9IYtSvWwVQtnzGHU=
github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc=
github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d h1:vtUKgx8dahOomfFzLREU8nSv25YHnTgLBn4rDnWZdU0=
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b h1:2n253B2r0pYSmEV+UNCQoPfU/FiaizQEK5Gu4Bq4JE8=
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM=
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/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/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

@ -0,0 +1,18 @@
{{define "base"}}
<!doctype html>
<html lang="en" data-theme="theme-dark">
<head>
<meta charset='utf-8'>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{template "title" .}}</title>
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<meta name="keywords" content="pastebin, paste, copy" />
{{template "meta" .}}
<link rel="stylesheet" href="/main.css" />
</head>
<body>{{template "body" .}}</body>
</html>
{{end}}

@ -0,0 +1,44 @@
{{template "base" .}}
{{define "title"}}{{.PageTitle}}{{end}}
{{define "meta"}}
<meta name="description" content="{{if .Header.Bio}}{{.Header.Bio}}{{else}}{{.Header.Title}}{{end}}" />
<meta property="og:type" content="website">
<meta property="og:site_name" content="{{.Site.Domain}}">
<meta property="og:url" content="{{.URL}}">
<meta property="og:title" content="{{.Header.Title}}">
{{if .Header.Bio}}<meta property="og:description" content="{{.Header.Bio}}">{{end}}
<meta property="og:image:width" content="300" />
<meta property="og:image:height" content="300" />
<meta itemprop="image" content="https://{{.Site.Domain}}/card.png" />
<meta property="og:image" content="https://{{.Site.Domain}}/card.png" />
<meta property="twitter:card" content="summary">
<meta property="twitter:url" content="{{.URL}}">
<meta property="twitter:title" content="{{.Header.Title}}">
{{if .Header.Bio}}<meta property="twitter:description" content="{{.Header.Bio}}">{{end}}
<meta name="twitter:image" content="https://{{.Site.Domain}}/card.png" />
<meta name="twitter:image:src" content="https://{{.Site.Domain}}/card.png" />
{{end}}
{{define "body"}}
<header class="text-center">
<h1 class="text-2xl font-bold">{{.Header.Title}}</h1>
<hr />
</header>
<main>
<section class="posts">
{{range .Posts}}
<article>
<div class="flex items-center">
<time datetime="{{.PublishAtISO}}" class="font-italic text-sm post-date">{{.PublishAt}}</time>
<h2 class="font-bold flex-1"><a href="{{.URL}}">{{.Title}}</a></h2>
</div>
</article>
{{end}}
</section>
</main>
{{template "footer" .}}
{{end}}

@ -0,0 +1,6 @@
{{define "footer"}}
<footer>
<hr />
published with <a href={{.Site.HomeURL}}>{{.Site.Domain}}</a>
</footer>
{{end}}

@ -0,0 +1,108 @@
{{template "base" .}}
{{define "title"}}help -- {{.Site.Domain}}{{end}}
{{define "meta"}}
<meta name="description" content="questions and answers" />
{{end}}
{{define "body"}}
<header>
<h1 class="text-2xl">Need help?</h1>
<p>Here are some common questions on using this platform that we would like to answer.</p>
</header>
<main>
<section id="permission-denied">
<h2 class="text-xl">I get a permission denied when trying to SSH</h2>
<p>
Unfortunately SHA-2 RSA keys are <strong>not</strong> currently supported.
</p>
<p>
Unfortunately, due to a shortcoming in Go’s x/crypto/ssh package, Soft Serve does
not currently support access via new SSH RSA keys: only the old SHA-1 ones will work.
Until we sort this out you’ll either need an SHA-1 RSA key or a key with another
algorithm, e.g. Ed25519. Not sure what type of keys you have? You can check with the
following:
</p>
<pre>$ find ~/.ssh/id_*.pub -exec ssh-keygen -l -f {} \;</pre>
<p>If you’re curious about the inner workings of this problem have a look at:</p>
<ul>
<li><a href="https://github.com/golang/go/issues/37278">golang/go#37278</a></li>
<li><a href="https://go-review.googlesource.com/c/crypto/+/220037">go-review</a></li>
<li><a href="https://github.com/golang/crypto/pull/197">golang/crypto#197</a></li>
</ul>
</section>
<section id="ssh-key">
<h2 class="text-xl">Generating a new SSH key</h2>
<p>
<a href="https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent">Github reference</a>
</p>
<pre>ssh-keygen -t ed25519 -C "your_email@example.com"</pre>
<ol>
<li>When you're prompted to "Enter a file in which to save the key," press Enter. This accepts the default file location.</li>
<li>At the prompt, type a secure passphrase.</li>
</ol>
</section>
<section id="post-update">
<h2 class="text-xl">How do I update a pastebin post?</h2>
<p>
Updating a post requires that you update the source document and then run the <code>scp</code>
command again. If the filename remains the same, then the post will be updated.
</p>
</section>
<section id="post-delete">
<h2 class="text-xl">How do I delete a post?</h2>
<p>
Because <code>scp</code> does not natively support deleting files, I didn't want to bake
that behavior into my ssh server.
</p>
<p>
However, if a user wants to delete a post they can delete the contents of the file and
then upload it to our server. If the file contains 0 bytes, we will remove the post.
For example, if you want to delete <code>delete.txt</code> you could:
</p>
<pre>
cp /dev/null delete.txt
scp ./delete.txt {{.Site.Domain}}:/</pre>
<p>
Alternatively, you can go to <code>ssh {{.Site.Domain}}</code> and select "Manage posts."
Then you can highlight the post you want to delete and then press "X." It will ask for
confirmation before actually removing the post.
</p>
</section>
<section id="post-upload-single-file">
<h2 class="text-xl">When I want to publish a new post, do I have to upload all posts everytime?</h2>
<p>
Nope! Just <code>scp</code> the file you want to publish. For example, if you created
a new post called <code>taco-tuesday.md</code> then you would publish it like this:
</p>
<pre>scp ./taco-tuesday.md {{.Site.Domain}}:</pre>
</section>
<section id="pastebin-url">
<h2 class="text-xl">What is my pastebin URL?</h2>
<pre>https://{username}.{{.Site.Domain}}</pre>
</section>
<section id="multiple-accounts">
<h2 class="text-xl">Can I create multiple accounts?</h2>
<p>
Yes! You can either a) create a new keypair and use that for authentication
or b) use the same keypair and ssh into our CMS using our special username
<code>ssh new@{{.Site.Domain}}</code>.
</p>
<p>
Please note that if you use the same keypair for multiple accounts, you will need to
always specify the user when logging into our CMS.
</p>
</section>
</main>
{{template "marketing-footer" .}}
{{end}}

@ -0,0 +1,12 @@
{{define "marketing-footer"}}
<footer>
<hr />
<p class="font-italic">Built and maintained by <a href="https://pico.sh">pico.sh</a>.</p>
<div>
<a href="/">home</a> |
<a href="/ops">ops</a> |
<a href="/help">help</a> |
<a href="https://git.sr.ht/~erock/pastes.sh">source</a>
</div>
</footer>
{{end}}

@ -0,0 +1,99 @@
{{template "base" .}}
{{define "title"}}{{.Site.Domain}} -- a pastebin for hackers{{end}}
{{define "meta"}}
<meta name="description" content="a pastebin for hackers" />
<meta property="og:type" content="website">
<meta property="og:site_name" content="{{.Site.Domain}}">
<meta property="og:url" content="https://{{.Site.Domain}}">
<meta property="og:title" content="{{.Site.Domain}}">
<meta property="og:description" content="a pastebin for hackers">
<meta name="twitter:card" content="summary" />
<meta property="twitter:url" content="https://{{.Site.Domain}}">
<meta property="twitter:title" content="{{.Site.Domain}}">
<meta property="twitter:description" content="a pastebin platform for hackers">
<meta name="twitter:image" content="https://{{.Site.Domain}}/card.png" />
<meta name="twitter:image:src" content="https://{{.Site.Domain}}/card.png" />
<meta property="og:image:width" content="300" />
<meta property="og:image:height" content="300" />
<meta itemprop="image" content="https://{{.Site.Domain}}/card.png" />
<meta property="og:image" content="https://{{.Site.Domain}}/card.png" />
{{end}}
{{define "body"}}
<header class="text-center">
<h1 class="text-2xl font-bold">{{.Site.Domain}}</h1>
<p class="text-lg">a pastebin platform for hackers</p>
<p class="text-lg"><a href="/read">discover</a> some interesting pastebins</p>
<hr />
</header>
<main>
<section>
<h2 class="text-lg font-bold">Create your account with Public-Key Cryptography</h2>
<p>We don't want your email address.</p>
<p>To get started, simply ssh into our content management system:</p>
<pre>ssh new@{{.Site.Domain}}</pre>
<div class="text-sm font-italic note">
note: <code>new</code> is a special username that will always send you to account
creation, even with multiple accounts associated with your key-pair.
</div>
<div class="text-sm font-italic note">
note: getting permission denied? <a href="/help#permission-denied">read this</a>
</div>
<p>
After that, just set a username and you're ready to start writing! When you SSH
again, use your username that you set in the CMS.
</p>
</section>
<section>
<h2 class="text-lg font-bold">Publish your pastes with one command</h2>
<p>
When your post is ready to be published, copy the file to our server with a familiar
command:
</p>
<pre>scp ~/pastes/* {{.Site.Domain}}:/</pre>
<p>We'll either create or update the posts for you.</p>
</section>
<section>
<h2 class="text-lg font-bold">Terminal workflow without installation</h2>
<p>
Since we are leveraging tools you already have on your computer
(<code>ssh</code> and <code>scp</code>), there is nothing to install.
</p>
<p>
This provides the convenience of a web app, but from inside your terminal!
</p>
</section>
<section>
<h2 class="text-lg font-bold">Features</h2>
<ul>
<li>Bring your own editor</li>
<li>You control the source files</li>
<li>Terminal workflow with no installation</li>
<li>Public-key based authentication</li>
<li>No ads, zero tracking</li>
<li>No platform lock-in</li>
<li>No javascript</li>
<li>Minimalist design</li>
<li>100% open source</li>
</ul>
</section>
<section>
<h2 class="text-lg font-bold">Roadmap</h2>
<ol>
<li>idk</li>
</ol>
</section>
</main>
{{template "marketing-footer" .}}
{{end}}

@ -0,0 +1,124 @@
{{template "base" .}}
{{define "title"}}operations -- {{.Site.Domain}}{{end}}
{{define "meta"}}
<meta name="description" content="{{.Site.Domain}} operations" />
{{end}}
{{define "body"}}
<header>
<h1 class="text-2xl">Operations</h1>
<ul>
<li><a href="/privacy">privacy</a></li>
<li><a href="/transparency">transparency</a></li>
</ul>
</header>
<main>
<section>
<h2 class="text-xl">Purpose</h2>
<p>
{{.Site.Domain}} exists to allow people to create and share their thoughts
without the need to set up their own server or be part of a platform
that shows ads or tracks its users.
</p>
</section>
<section>
<h2 class="text-xl">Ethics</h2>
<p>We are committed to:</p>
<ul>
<li>No tracking of user or visitor behaviour.</li>
<li>Never sell any user or visitor data.</li>
<li>No ads — ever.</li>
</ul>
</section>
<section>
<h2 class="text-xl">Code of Content Publication</h2>
<p>
Content in {{.Site.Domain}} pastes is unfiltered and unmonitored. Users are free to publish any
combination of words and pixels except for: content of animosity or disparagement of an
individual or a group on account of a group characteristic such as race, color, national
origin, sex, disability, religion, or sexual orientation, which will be taken down
immediately.
</p>
<p>
If one notices something along those lines in a paste post please let us know at
<a href="mailto:{{.Site.Email}}">{{.Site.Email}}</a>.
</p>
</section>
<section>
<h2 class="text-xl">Liability</h2>
<p>
The user expressly understands and agrees that Eric Bower and Antonio Mika, the operator of this website
shall not be liable, in law or in equity, to them or to any third party for any direct,
indirect, incidental, lost profits, special, consequential, punitive or exemplary damages.
</p>
</section>
<section>
<h2 class="text-xl">Account Terms</h2>
<p>
<ul>
<li>
The user is responsible for all content posted and all actions performed with
their account.
</li>
<li>
We reserve the right to disable or delete a user's account for any reason at
any time. We have this clause because, statistically speaking, there will be
people trying to do something nefarious.
</li>
</ul>
</p>
</section>
<section>
<h2 class="text-xl">Service Availability</h2>
<p>
We provide the {{.Site.Domain}} service on an "as is" and "as available" basis. We do not offer
service-level agreements but do take uptime seriously.
</p>
</section>
<section>
<h2 class="text-xl">Contact and Support</h2>
<p>
Email us at <a href="mailto:{{.Site.Email}}">{{.Site.Email}}</a>
with any questions.
</p>
</section>
<section>
<h2 class="text-xl">Acknowledgments</h2>
<p>
{{.Site.Domain}} was inspired by <a href="https://mataroa.blog">Mataroa Blog</a>
and <a href="https://bearblog.dev/">Bear Blog</a>.
</p>
<p>
{{.Site.Domain}} is built with many open source technologies.
</p>
<p>
In particular we would like to thank:
</p>
<ul>
<li>
<span>The </span>
<a href="https://charm.sh">charm.sh</a>
<span> community</span>
</li>
<li>
<span>The </span>
<a href="https://go.dev">golang</a>
<span> community</span>
</li>
<li>
<span>The </span>
<a href="https://www.postgresql.org/">postgresql</a>
<span> community</span>
</li>
<li>
<span>The </span>
<a href="https://github.com/caddyserver/caddy">caddy</a>
<span> community</span>
</li>
</ul>
</section>
</main>
{{template "marketing-footer" .}}
{{end}}

@ -0,0 +1,36 @@
{{template "base" .}}
{{define "title"}}{{.PageTitle}}{{end}}
{{define "meta"}}
<meta property="og:type" content="website">
<meta property="og:site_name" content="{{.Site.Domain}}">
<meta property="og:url" content="{{.URL}}">
<meta property="og:title" content="{{.Title}}">
<meta property="og:image:width" content="300" />
<meta property="og:image:height" content="300" />
<meta itemprop="image" content="https://{{.Site.Domain}}/card.png" />
<meta property="og:image" content="https://{{.Site.Domain}}/card.png" />
<meta property="twitter:card" content="summary">
<meta property="twitter:url" content="{{.URL}}">
<meta property="twitter:title" content="{{.Title}}">
<meta name="twitter:image" content="https://{{.Site.Domain}}/card.png" />
<meta name="twitter:image:src" content="https://{{.Site.Domain}}/card.png" />
{{end}}
{{define "body"}}
<header>
<h1 class="text-2xl font-bold">{{.Title}}</h1>
<p class="font-bold m-0">
<time datetime="{{.PublishAtISO}}">{{.PublishAt}}</time>
<span> on </span>
<a href="{{.BlogURL}}">{{.BlogName}}</a></p>
</header>
<main>
<article>
{{.Contents}}
</article>
</main>
{{template "footer" .}}
{{end}}

@ -0,0 +1,52 @@
{{template "base" .}}
{{define "title"}}privacy -- {{.Site.Domain}}{{end}}
{{define "meta"}}
<meta name="description" content="{{.Site.Domain}} privacy policy" />
{{end}}
{{define "body"}}
<header>
<h1 class="text-2xl">Privacy</h1>
<p>Details on our privacy and security approach.</p>
</header>
<main>
<section>
<h2 class="text-xl">Account Data</h2>
<p>
In order to have a functional account at {{.Site.Domain}}, we need to store
your public key. That is the only piece of information we record for a user.
</p>
<p>
Because we use public-key cryptography, our security posture is a battle-tested
and proven technique for authentication.
</p>
</section>
<section>
<h2 class="text-xl">Third parties</h2>
<p>
We have a strong commitment to never share any user data with any third-parties.
</p>
</section>
<section>
<h2 class="text-xl">Service Providers</h2>
<ul>
<li>
<span>We host our server on </span>
<a href="https://digitalocean.com">digital ocean</a>
</li>
</ul>
</section>
<section>
<h2 class="text-xl">Cookies</h2>
<p>
We do not use any cookies, not even account authentication.
</p>
</section>
</main>
{{template "marketing-footer" .}}
{{end}}

@ -0,0 +1,35 @@
{{template "base" .}}
{{define "title"}}discover -- {{.Site.Domain}}{{end}}
{{define "meta"}}
<meta name="description" content="discover interesting posts" />
{{end}}
{{define "body"}}
<header class="text-center">
<h1 class="text-2xl font-bold">read</h1>
<p class="text-lg">recent pastes</p>
<hr />
</header>
<main>
<div class="my">
{{if .PrevPage}}<a href="{{.PrevPage}}">prev</a>{{else}}<span class="text-grey">prev</span>{{end}}
{{if .NextPage}}<a href="{{.NextPage}}">next</a>{{else}}<span class="text-grey">next</span>{{end}}
</div>
{{range .Posts}}
<article>
<div class="flex items-center">
<time datetime="{{.PublishAtISO}}" class="font-italic text-sm post-date">{{.PublishAt}}</time>
<div class="flex-1">
<h2 class="inline"><a href="{{.URL}}">{{.Title}}</a></h2>
<address class="text-sm inline">
<a href="{{.BlogURL}}" class="link-grey">({{.Username}})</a>
</address>
</div>
</div>
</article>
{{end}}
</main>
{{template "marketing-footer" .}}
{{end}}

@ -0,0 +1 @@
{{.Contents}}

@ -0,0 +1,57 @@
{{template "base" .}}
{{define "title"}}transparency -- {{.Site.Domain}}{{end}}
{{define "meta"}}
<meta name="description" content="full transparency of analytics and cost at {{.Site.Domain}}" />
{{end}}
{{define "body"}}
<header>
<h1 class="text-2xl">Transparency</h1>
<hr />
</header>
<main>
<section>
<h2 class="text-xl">Analytics</h2>
<p>
Here are some interesting stats on usage.
</p>
<article>
<h2 class="text-lg">Total users</h2>
<div>{{.Analytics.TotalUsers}}</div>
</article>
<article>
<h2 class="text-lg">New users in the last month</h2>
<div>{{.Analytics.UsersLastMonth}}</div>
</article>
<article>
<h2 class="text-lg">Total pastes</h2>
<div>{{.Analytics.TotalPosts}}</div>
</article>
<article>
<h2 class="text-lg">New pastes in the last month</h2>
<div>{{.Analytics.PostsLastMonth}}</div>
</article>
<article>
<h2 class="text-lg">Users with at least one paste</h2>
<div>{{.Analytics.UsersWithPost}}</div>
</article>
</section>
<section>
<h2 class="text-xl">Service maintenance costs</h2>
<ul>
<li>Server $5.00/mo</li>
<li>Domain name $3.25/mo</li>
<li>Programmer $0.00/mo</li>
</ul>
</section>
</main>
{{template "marketing-footer" .}}
{{end}}

@ -0,0 +1,499 @@
package internal
import (
"fmt"
"html/template"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"time"
"git.sr.ht/~erock/wish/cms/db"
"git.sr.ht/~erock/wish/cms/db/postgres"
)
type PageData struct {
Site SitePageData
}
type PostItemData struct {
URL template.URL
BlogURL template.URL
Username string
Title string
Description string
PublishAtISO string
PublishAt string
UpdatedAtISO string
UpdatedTimeAgo string
Padding string
}
type BlogPageData struct {
Site SitePageData
PageTitle string
URL template.URL
RSSURL template.URL
Username string
Header *HeaderTxt
Posts []PostItemData
}
type ReadPageData struct {
Site SitePageData
NextPage string
PrevPage string
Posts []PostItemData
}
type PostPageData struct {
Site SitePageData
PageTitle string
URL template.URL
BlogURL template.URL
Title string
Description string
Username string
BlogName string
Contents template.HTML
PublishAtISO string
PublishAt string
}
type TransparencyPageData struct {
Site SitePageData
Analytics *db.Analytics
}
func renderTemplate(templates []string) (*template.Template, error) {
files := make([]string, len(templates))
copy(files, templates)
files = append(
files,
"./html/footer.partial.tmpl",
"./html/marketing-footer.partial.tmpl",
"./html/base.layout.tmpl",
)
ts, err := template.ParseFiles(files...)
if err != nil {
return nil, err
}
return ts, nil
}
func createPageHandler(fname string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
logger := GetLogger(r)
cfg := GetCfg(r)
ts, err := renderTemplate([]string{fname})
if err != nil {
logger.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data := PageData{
Site: *cfg.GetSiteData(),
}
err = ts.Execute(w, data)
if err != nil {
logger.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}
type Link struct {
URL string
Text string
}
type HeaderTxt struct {
Title string
Bio string
Nav []Link
HasLinks bool
}
type ReadmeTxt struct {
HasText bool
Contents template.HTML
}
func GetUsernameFromRequest(r *http.Request) string {
subdomain := GetSubdomain(r)
cfg := GetCfg(r)
if !cfg.IsSubdomains() || subdomain == "" {
return GetField(r, 0)
}
return subdomain
}
func blogHandler(w http.ResponseWriter, r *http.Request) {
username := GetUsernameFromRequest(r)
dbpool := GetDB(r)
logger := GetLogger(r)
cfg := GetCfg(r)
user, err := dbpool.FindUserForName(username)
if err != nil {
logger.Infof("blog not found: %s", username)
http.Error(w, "blog not found", http.StatusNotFound)
return
}
posts, err := dbpool.FindPostsForUser(user.ID)
if err != nil {
logger.Error(err)
http.Error(w, "could not fetch posts for blog", http.StatusInternalServerError)
return
}
ts, err := renderTemplate([]string{
"./html/blog.page.tmpl",
})
if err != nil {
logger.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
headerTxt := &HeaderTxt{
Title: GetBlogName(username),
Bio: "",
}
postCollection := make([]PostItemData, 0, len(posts))
for _, post := range posts {
p := PostItemData{
URL: template.URL(cfg.PostURL(post.Username, post.Filename)),
BlogURL: template.URL(cfg.BlogURL(post.Username)),
Title: FilenameToTitle(post.Filename, post.Title),
PublishAt: post.PublishAt.Format("02 Jan, 2006"),
PublishAtISO: post.PublishAt.Format(time.RFC3339),
UpdatedTimeAgo: TimeAgo(post.UpdatedAt),
UpdatedAtISO: post.UpdatedAt.Format(time.RFC3339),