This commit is contained in:
Cat /dev/Nulo 2023-01-19 20:26:46 -03:00
commit 4be58013b6
5 changed files with 287 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
files/
db.db

10
go.mod Normal file
View file

@ -0,0 +1,10 @@
module gitea.nulo.in/Nulo/cacher
go 1.19
require (
gitea.nulo.in/Nulo/go-migrate v0.0.4 // indirect
github.com/jmoiron/sqlx v1.3.5 // indirect
github.com/matoous/go-nanoid/v2 v2.0.0 // indirect
github.com/mattn/go-sqlite3 v1.14.16 // indirect
)

36
go.sum Normal file
View file

@ -0,0 +1,36 @@
gitea.nulo.in/Nulo/go-migrate v0.0.3 h1:Y9b7Jix1CSZturT9t5Dw8XooSOJt38e2vsFu+e8j+FM=
gitea.nulo.in/Nulo/go-migrate v0.0.3/go.mod h1:7omz1cegf/PsF9xe7uMrnkAMFjlkaVDNzqD/o2RZjkc=
gitea.nulo.in/Nulo/go-migrate v0.0.4 h1:/6rnMJciD5CDfxYUFb9FWCxgFxb+5tEusIcTIbf7irA=
gitea.nulo.in/Nulo/go-migrate v0.0.4/go.mod h1:kSNRgNsHB+6SkZz4vnjih+sfruILdEBvBhHOHUMnZO0=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/matoous/go-nanoid v1.5.0 h1:VRorl6uCngneC4oUQqOYtO3S0H5QKFtKuKycFG3euek=
github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U=
github.com/matoous/go-nanoid/v2 v2.0.0 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL+9Tj0=
github.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

83
main.go Normal file
View file

@ -0,0 +1,83 @@
package main
import (
"fmt"
"log"
"net/http"
"os"
migrate "gitea.nulo.in/Nulo/go-migrate"
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
)
var admin_secret string
func main() {
if len(os.Args) < 2 {
log.Fatal("Missing port argument")
}
admin_secret = os.Getenv("CACHER_ADMIN_SECRET")
if len(admin_secret) == 0 {
log.Fatal("Missing CACHER_ADMIN_SECRET")
}
err := os.MkdirAll("./files/", 0700)
if err != nil {
log.Fatalf("MkdirAll: %w", err)
}
db := sqlx.MustConnect("sqlite3", "file:./db.db?_foreign_keys=true")
migrator := migrate.Sqlx{
Migrations: []migrate.SqlxMigration{
migrate.SqlxQueryMigration(
"0001_create_caches",
`create table caches(
name text not null primary key,
secret text not null
);`,
"",
),
},
}
err = migrator.Migrate(db.DB, "sqlite3")
if err != nil {
log.Fatalf("Migrate() err = %v; want nil", err)
}
http.HandleFunc("/", index)
http.Handle("/cache/", &cache{db: db})
http.ListenAndServe(os.Args[1], nil)
}
func index(w http.ResponseWriter, req *http.Request) {
w.Header().Add("Content-Type", "text/html; charset=utf-8")
fmt.Fprintf(w, `<!doctype html>
<title>cacher API</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<h1><a href="https://gitea.nulo.in/Nulo/cacher">cacher</a></h1>
<h2><code>POST /cache/$NAME</code></h2>
<ul>
<li><code>Authorization: Bearer $ADMIN_SECRET</code></li>
</ul>
<p>Crea el cache y devuelve un JSON con el secreto.</p>
<pre>curl --request POST -H "Authorization: Bearer $ADMIN_SECRET" $URL/cache/$CACHE_NAME</pre>
<h2><code>PUT /cache/$NAME</code></h2>
<ul>
<li><code>Authorization: Bearer $CACHE_SECRET</code></li>
<li>Pasar archivo como body (no es multipart)</li>
</ul>
<p>Guarda en el cache un archivo arbitrario.</p>
<pre>curl --request PUT --upload-file $ARCHIVO -H "Authorization: Bearer $CACHE_SECRET" $URL/cache/$CACHE_NAME</pre>
<h2><code>GET /cache/$NAME</code></h2>
<ul>
<li><code>Authorization: Bearer $CACHE_SECRET</code></li>
</ul>
<p>Envía el archivo en el cache.</p>
<pre>curl --output $FILE -H "Authorization: Bearer $CACHE_SECRET" $URL/cache/$CACHE_NAME</pre>
`)
}

156
save_cache.go Normal file
View file

@ -0,0 +1,156 @@
package main
import (
"database/sql"
"encoding/json"
"io"
"log"
"net/http"
"os"
"path"
"strings"
"github.com/jmoiron/sqlx"
gonanoid "github.com/matoous/go-nanoid/v2"
"github.com/mattn/go-sqlite3"
)
func badRequest(w http.ResponseWriter) {
http.Error(w, "bad request", http.StatusBadRequest)
}
func getAdminAuth(w http.ResponseWriter, req *http.Request) bool {
auth := req.Header.Get("Authorization")
if len(auth) == 0 {
badRequest(w)
return false
}
if auth == "Bearer "+admin_secret {
return true
}
return false
}
func (r *cache) getCache(w http.ResponseWriter, req *http.Request, name string) *dbCache {
auth := req.Header.Get("Authorization")
if len(auth) == 0 {
badRequest(w)
return nil
}
var c dbCache
err := r.db.Get(&c, "SELECT secret FROM caches WHERE name = ?", name)
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "cache doesn't exist", http.StatusNotFound)
} else {
log.Printf("Get(SELECT): %w", err)
http.Error(w, "server error", http.StatusInternalServerError)
}
return nil
}
if auth == "Bearer "+c.Secret {
return &c
} else {
http.Error(w, "wrong secret", http.StatusUnauthorized)
return nil
}
}
type cache struct {
db *sqlx.DB
}
type dbCache struct {
Secret string
}
type responseNewCache struct {
Secret string `json:"secret"`
}
func (r *cache) ServeHTTP(w http.ResponseWriter, req *http.Request) {
parts := strings.Split(req.URL.Path[1:], "/")
log.Println(parts)
if len(parts) != 2 {
http.Error(w, "wrong amount of parts", http.StatusBadRequest)
return
}
name := parts[1]
if req.Method == "POST" {
if !getAdminAuth(w, req) {
return
}
if strings.ContainsAny(name, "/") {
badRequest(w)
return
}
secret, err := gonanoid.New()
if err != nil {
log.Printf("Error generating ID: %w", err)
http.Error(w, "server error", http.StatusInternalServerError)
return
}
_, err = r.db.Exec("INSERT INTO caches(name, secret) VALUES(?, ?)", name, secret)
if err != nil {
if err.(sqlite3.Error).Code == sqlite3.ErrConstraint {
http.Error(w, "cache already exists", http.StatusConflict)
} else {
log.Printf("Error saving cache DB entry: %w", err)
http.Error(w, "server error", http.StatusInternalServerError)
}
return
}
byt, err := json.Marshal(responseNewCache{Secret: secret})
if err != nil {
log.Printf("Marshal: %w", err)
http.Error(w, "server error", http.StatusInternalServerError)
return
}
w.Header().Add("Content-Encoding", "application/json")
w.Write(byt)
} else if req.Method == "PUT" {
c := r.getCache(w, req, name)
if c == nil {
return
}
file, err := os.Create(path.Join("./files/", name))
if err != nil {
log.Printf("os.Create: %w", err)
http.Error(w, "server error", http.StatusInternalServerError)
return
}
_, err = io.Copy(file, req.Body)
if err != nil {
log.Printf("io.Copy: %w", err)
http.Error(w, "server error", http.StatusInternalServerError)
return
}
} else if req.Method == "GET" {
c := r.getCache(w, req, name)
if c == nil {
return
}
file, err := os.Open(path.Join("./files/", name))
if err != nil {
log.Printf("os.Open: %w", err)
http.Error(w, "server error", http.StatusInternalServerError)
return
}
_, err = io.Copy(w, file)
if err != nil {
log.Printf("io.Copy: %w", err)
http.Error(w, "server error", http.StatusInternalServerError)
return
}
} else {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
}