Compare commits

...

10 Commits

Author SHA1 Message Date
breel
4b7b3e3c68 pushing 2021-09-08 14:10:47 -06:00
breel
7ce2104b61 New deploy 2020-08-28 16:29:12 -06:00
breel
002595a407 Serve /index.html on 404 2020-08-28 16:16:35 -06:00
breel
4d8f75f7c9 Accept ext on entity files to set content type in resp 2020-08-28 16:07:24 -06:00
breel
3a3ee3912d Update swagger for json login, api prefix, direct upload, markdown read 2020-08-28 15:59:44 -06:00
breel
cf3a289a54 404 to static file server 2020-08-28 15:43:25 -06:00
breel
ec5223d530 optional API path prefix 2020-08-28 15:28:47 -06:00
breel
25a43c8a0b Impl direct uplod 2020-08-28 14:47:18 -06:00
breel
c2cb535105 markdown param for get one 2020-08-28 14:38:15 -06:00
breel
d67654e601 For storage, store as JSON rather than BSON 2020-08-27 15:29:17 -06:00
32 changed files with 521 additions and 76 deletions

View File

@@ -4,6 +4,7 @@ import (
"io/ioutil" "io/ioutil"
"local/args" "local/args"
"os" "os"
"path"
"strings" "strings"
"time" "time"
) )
@@ -13,6 +14,7 @@ type Config struct {
Database string Database string
Driver []string Driver []string
FilePrefix string FilePrefix string
APIPrefix string
FileRoot string FileRoot string
Auth bool Auth bool
AuthLifetime time.Duration AuthLifetime time.Duration
@@ -20,6 +22,7 @@ type Config struct {
RPS int RPS int
SysRPS int SysRPS int
Delay time.Duration Delay time.Duration
StaticRoot string
} }
func New() Config { func New() Config {
@@ -33,6 +36,8 @@ func New() Config {
as.Append(args.INT, "p", "port to listen on", 18114) as.Append(args.INT, "p", "port to listen on", 18114)
as.Append(args.STRING, "fileprefix", "path prefix for file service", "/__files__") as.Append(args.STRING, "fileprefix", "path prefix for file service", "/__files__")
as.Append(args.STRING, "api-prefix", "path prefix for api", "api")
as.Append(args.STRING, "static-root", "path to the root of a static file server", "./public")
as.Append(args.STRING, "fileroot", "path to file hosting root", "/tmp/") as.Append(args.STRING, "fileroot", "path to file hosting root", "/tmp/")
as.Append(args.STRING, "database", "database name to use", "db") as.Append(args.STRING, "database", "database name to use", "db")
as.Append(args.STRING, "driver", "database driver args to use, like [local/storage.Type,arg1,arg2...] or [/path/to/boltdb]", "map") as.Append(args.STRING, "driver", "database driver args to use, like [local/storage.Type,arg1,arg2...] or [/path/to/boltdb]", "map")
@@ -60,5 +65,7 @@ func New() Config {
MaxFileSize: int64(as.GetInt("max-file-size")), MaxFileSize: int64(as.GetInt("max-file-size")),
RPS: as.GetInt("rps"), RPS: as.GetInt("rps"),
SysRPS: as.GetInt("sys-rps"), SysRPS: as.GetInt("sys-rps"),
APIPrefix: strings.TrimPrefix(as.GetString("api-prefix"), "/"),
StaticRoot: path.Join(as.GetString("static-root")),
} }
} }

View File

@@ -8,7 +8,6 @@ function main() {
go test ./... go test ./...
fi fi
GOOS=linux GOARCH=arm GOARM=5 gobuild $exec GOOS=linux GOARCH=arm GOARM=5 gobuild $exec
scp -r ./public/swagger/* zach@tickle.lan:./dndex1/files/swagger/
scp $exec zach@tickle.lan:./dndex1/dndex.new scp $exec zach@tickle.lan:./dndex1/dndex.new
ssh zach@tickle.lan bash -c "true; while [ -e ./dndex1/dndex.new ]; do printf '.'; sleep 3; done; echo" ssh zach@tickle.lan bash -c "true; while [ -e ./dndex1/dndex.new ]; do printf '.'; sleep 3; done; echo"
if [ -n "$BIG" ]; then if [ -n "$BIG" ]; then
@@ -17,6 +16,9 @@ function main() {
echo ssh bel@remote.blapointe.com bash -c 'true; md5sum ./services/bin/dndex*; while [ -e ./services/bin/dndex.new ]; do printf "."; sleep 3; done; md5sum ./services/bin/dndex*' echo ssh bel@remote.blapointe.com bash -c 'true; md5sum ./services/bin/dndex*; while [ -e ./services/bin/dndex.new ]; do printf "."; sleep 3; done; md5sum ./services/bin/dndex*'
big big
fi fi
ssh zach@tickle.lan bash -c "true; rm -rf ./dndex1/files/swagger/"
scp -r ./public/swagger/ zach@tickle.lan:./dndex1/files/swagger/
ssh zach@tickle.lan bash -c "true; cd ./dndex1/public; rm -rf ./swagger; ln -s ../files/swagger"
rm $exec rm $exec
} }
@@ -42,7 +44,10 @@ function big() {
npm install npm install
npm run build npm run build
ssh zach@tickle.lan rm -rf ./dndex-ui/public ssh zach@tickle.lan rm -rf ./dndex-ui/public
ssh zach@tickle.lan bash -c "true; rm -rf ./dndex-ui/public"
scp -r ./dist zach@tickle.lan:./dndex-ui/public scp -r ./dist zach@tickle.lan:./dndex-ui/public
ssh zach@tickle.lan bash -c "true; rm -rf ./dndex1/public"
scp -r ./dist zach@tickle.lan:./dndex1/public
popd popd
} }

View File

@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"local/dndex/config"
"local/dndex/server/auth" "local/dndex/server/auth"
"local/dndex/storage/entity" "local/dndex/storage/entity"
"net/http" "net/http"
@@ -27,6 +28,8 @@ func Test(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(nil)) s := httptest.NewServer(http.HandlerFunc(nil))
s.Close() s.Close()
p := strings.Split(s.URL, ":")[2] p := strings.Split(s.URL, ":")[2]
os.Args = []string{"a"}
s.URL = s.URL + "/" + config.New().APIPrefix
os.Args = strings.Split(fmt.Sprintf(`dndex -auth=true -database db -delay 5ms -driver map -fileprefix /files -fileroot %s -p %v -rps 50 -sys-rps 40`, d, p), " ") os.Args = strings.Split(fmt.Sprintf(`dndex -auth=true -database db -delay 5ms -driver map -fileprefix /files -fileroot %s -p %v -rps 50 -sys-rps 40`, d, p), " ")
go main() go main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 B

View File

@@ -18,6 +18,7 @@ paths:
parameters: parameters:
- $ref: "#/components/parameters/token" - $ref: "#/components/parameters/token"
- $ref: "#/components/parameters/id" - $ref: "#/components/parameters/id"
- $ref: "#/components/parameters/md"
responses: responses:
200: 200:
$ref: "#/components/schemas/responseOneResolved" $ref: "#/components/schemas/responseOneResolved"
@@ -54,6 +55,13 @@ components:
$ref: "../swagger.yaml#/components/parameters/token" $ref: "../swagger.yaml#/components/parameters/token"
id: id:
$ref: "../swagger.yaml#/components/parameters/id" $ref: "../swagger.yaml#/components/parameters/id"
md:
name: md
description: "render the text section as markdown"
in: query
required: false
schema:
type: bool
schemas: schemas:
requestOne: requestOne:

View File

@@ -5,6 +5,7 @@ paths:
- files - files
parameters: parameters:
- $ref: "#/components/parameters/token" - $ref: "#/components/parameters/token"
- $ref: "#/components/parameters/direct"
requestBody: requestBody:
$ref: "#/components/schemas/requestForm" $ref: "#/components/schemas/requestForm"
@@ -12,6 +13,13 @@ components:
parameters: parameters:
token: token:
$ref: "../swagger.yaml#/components/parameters/token" $ref: "../swagger.yaml#/components/parameters/token"
direct:
name: direct
description: "interpret content as a direct link to actual content"
in: query
required: false
schema:
type: bool
schemas: schemas:
requestForm: requestForm:
$ref: "../swagger.yaml#/components/schemas/requestForm" $ref: "../swagger.yaml#/components/schemas/requestForm"

View File

@@ -0,0 +1,6 @@
paths:
get:
tags: []
summary: "Access a static read-only file server"
responses:
200: {}

View File

@@ -15,24 +15,26 @@ servers:
- url: http://api1.dndex.lan:8080/ - url: http://api1.dndex.lan:8080/
paths: paths:
/version: /api/version:
$ref: "./version.yaml#/paths" $ref: "./version.yaml#/paths"
/dump: /api/dump:
$ref: "./dump.yaml#/paths" $ref: "./dump.yaml#/paths"
/files: /api/files:
$ref: "./files/index.yaml#/paths" $ref: "./files/index.yaml#/paths"
/files/{path}: /api/files/{path}:
$ref: "./files/one.yaml#/paths" $ref: "./files/one.yaml#/paths"
/users/register: /api/users/register:
$ref: "./users/register.yaml#/paths" $ref: "./users/register.yaml#/paths"
/users/login: /api/users/login:
$ref: "./users/login.yaml#/paths" $ref: "./users/login.yaml#/paths"
/entities: /api/entities:
$ref: "./entities/index.yaml#/paths" $ref: "./entities/index.yaml#/paths"
/entities/{id}: /api/entities/{id}:
$ref: "./entities/id.yaml#/paths" $ref: "./entities/id.yaml#/paths"
/entities/{id}/{path}: /api/entities/{id}/{path}:
$ref: "./entities/idsub.yaml#/paths" $ref: "./entities/idsub.yaml#/paths"
/:
$ref: "./index.yaml#/paths"
components: components:
parameters: parameters:

View File

@@ -6,13 +6,10 @@ paths:
- users - users
requestBody: requestBody:
content: content:
application/json:
$ref: "#/components/schemas/requestLogin"
application/x-www-form-urlencoded: application/x-www-form-urlencoded:
schema: $ref: "#/components/schemas/requestLogin"
type: object
properties:
DnDex-User:
type: string
example: "namespace"
responses: responses:
200: 200:
content: content:
@@ -26,3 +23,13 @@ paths:
salt: salt:
type: string type: string
example: def-456 example: def-456
components:
schemas:
requestLogin:
schema:
type: object
properties:
DnDex-User:
type: string
example: "namespace"

View File

@@ -5,7 +5,19 @@ paths:
- users - users
requestBody: requestBody:
content: content:
application/json:
$ref: "#/components/schemas/requestRegister"
application/x-www-form-urlencoded: application/x-www-form-urlencoded:
$ref: "#/components/schemas/requestRegister"
responses:
200:
$ref: "#/components/schemas/responseOK"
components:
schemas:
responseOK:
$ref: "../swagger.yaml#/components/schemas/responseOK"
requestRegister:
schema: schema:
type: object type: object
properties: properties:
@@ -15,11 +27,3 @@ paths:
DnDex-Auth: DnDex-Auth:
type: string type: string
example: "password" example: "password"
responses:
200:
$ref: "#/components/schemas/responseOK"
components:
schemas:
responseOK:
$ref: "../swagger.yaml#/components/schemas/responseOK"

42
public/ui/index.html Normal file
View File

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DnDex UI</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css">
<script src="/ui/dndex.js"></script>
</head>
<body class="has-navbar-fixed-top">
<nav class="navbar is-fixed-top is-primary is-bold" role="navigation">
<div class="navbar-brand"></div>
<div class="navbar-menu">
<div class="navbar-start">
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">top</a>
<div class="navbar-dropdown">
<a class="navbar-item">a</a>
<hr class="navbar-divider">
<a class="navbar-item">b</a>
</div>
</div>
</div>
<div class="navbar-end">
<div class="navbar-item">
<div class="buttons">
<a class="button">Sign Up</a>
<a class="button">Log In</a>
</div>
</div>
</div>
</div>
</nav>
<section class="hero is-primary is-bold is-fullheight-with-navbar">
<div class="hero-head">
<div class="container">
<div class="title">Hero</div>
</div>
</div>
</section>
</body>
</html>

Submodule public/vue/dndex-ui updated: 72595fae58...70133bf814

View File

@@ -1,13 +1,19 @@
package auth package auth
import ( import (
"bytes"
"context" "context"
"encoding/json"
"errors" "errors"
"fmt"
"io"
"io/ioutil"
"local/dndex/storage" "local/dndex/storage"
"local/dndex/storage/entity" "local/dndex/storage/entity"
"net/http" "net/http"
"github.com/google/uuid" "github.com/google/uuid"
"gopkg.in/mgo.v2/bson"
) )
func GeneratePlain(g storage.RateLimitedGraph, r *http.Request) (string, error) { func GeneratePlain(g storage.RateLimitedGraph, r *http.Request) (string, error) {
@@ -47,8 +53,27 @@ func readRequestedNamespace(r *http.Request) string {
} }
func readRequested(r *http.Request, key string) string { func readRequested(r *http.Request, key string) string {
switch r.Header.Get("Content-Type") {
case "application/json":
b, _ := ioutil.ReadAll(r.Body)
r.Body = struct {
io.Reader
io.Closer
}{
Reader: bytes.NewReader(b),
Closer: r.Body,
}
m := bson.M{}
json.Unmarshal(b, &m)
v, ok := m[key]
if !ok {
return ""
}
return fmt.Sprint(v)
default:
return r.FormValue(key) return r.FormValue(key)
} }
}
func getKeyForNamespace(ctx context.Context, g storage.RateLimitedGraph, namespace string) (string, error) { func getKeyForNamespace(ctx context.Context, g storage.RateLimitedGraph, namespace string) (string, error) {
namespaceOne, err := g.Get(ctx, toAuthNamespace(namespace), UserKey) namespaceOne, err := g.Get(ctx, toAuthNamespace(namespace), UserKey)

View File

@@ -95,3 +95,30 @@ func TestGenerate(t *testing.T) {
} }
}) })
} }
func TestReadRequested(t *testing.T) {
t.Run("form: ignore query params", func(t *testing.T) {
r := httptest.NewRequest(http.MethodPost, "/a=c", nil)
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
if got := readRequested(r, "a"); got != "" {
t.Fatal(got)
}
})
t.Run("form: body beats query params", func(t *testing.T) {
r := httptest.NewRequest(http.MethodPost, "/a=c", strings.NewReader(`a=b`))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
if got := readRequested(r, "a"); got != "b" {
t.Fatal(got)
}
})
t.Run("json: OK", func(t *testing.T) {
r := httptest.NewRequest(http.MethodPost, "/a=c", strings.NewReader(`{"a": "b"}`))
r.Header.Set("Content-Type", "application/json")
if got := readRequested(r, "a"); got != "b" {
t.Fatal(got)
}
})
}

View File

@@ -10,6 +10,9 @@ import (
"path" "path"
"strings" "strings"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/html"
"github.com/gomarkdown/markdown/parser"
"github.com/google/uuid" "github.com/google/uuid"
"go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/bson/primitive"
"gopkg.in/mgo.v2/bson" "gopkg.in/mgo.v2/bson"
@@ -119,6 +122,12 @@ func (rest *REST) entitiesGetOne(w http.ResponseWriter, r *http.Request) {
} }
resp[entity.Connections] = m resp[entity.Connections] = m
} }
_, md := r.URL.Query()["md"]
if md {
renderer := html.NewRenderer(html.RendererOptions{Flags: html.CommonFlags | html.TOC})
parser := parser.NewWithExtensions(parser.CommonExtensions | parser.HeadingIDs | parser.AutoHeadingIDs | parser.Titleblock)
resp["md"] = markdownHead + string(markdown.ToHTML([]byte(one.Text), parser, renderer)) + markdownTail
}
rest.respMap(w, entityScope[0], resp) rest.respMap(w, entityScope[0], resp)
} }

View File

@@ -26,10 +26,21 @@ func TestEntities(t *testing.T) {
return one.Name == "myname" return one.Name == "myname"
}) })
w = testEntitiesMethod(t, authit, rest, http.MethodGet, "/"+id, ``) w = testEntitiesMethod(t, authit, rest, http.MethodGet, "/"+id+"?md", ``)
if w.Code != http.StatusOK { if w.Code != http.StatusOK {
t.Fatal(w.Code) t.Fatal(w.Code)
} }
b := w.Body.Bytes()
var markdowned map[string]struct {
MD string `json:"md"`
}
if err := json.Unmarshal(b, &markdowned); err != nil {
t.Fatal(err)
} else if len(markdowned) != 1 {
t.Fatal(len(markdowned))
} else if len(markdowned[id].MD) == 0 {
t.Fatal(markdowned, string(b))
}
id2 := testEntitiesGetOneResponse(t, w.Body, func(one entity.One) bool { id2 := testEntitiesGetOneResponse(t, w.Body, func(one entity.One) bool {
return one.Name == "myname" return one.Name == "myname"
}) })

View File

@@ -2,10 +2,13 @@ package server
import ( import (
"io" "io"
"io/ioutil"
"local/dndex/config" "local/dndex/config"
"local/simpleserve/simpleserve"
"net/http" "net/http"
"os" "os"
"path" "path"
"strings"
"github.com/google/uuid" "github.com/google/uuid"
) )
@@ -50,12 +53,34 @@ func (rest *REST) filesCreate(w http.ResponseWriter, r *http.Request) {
return return
} }
defer f.Close() defer f.Close()
if _, err := io.Copy(f, r.Body); err != nil { if err := rest.filesStream(r, f); err != nil {
rest.respError(w, err) rest.respError(w, err)
return
} }
w.Write([]byte(id)) w.Write([]byte(id))
} }
func (rest *REST) filesStream(r *http.Request, f io.Writer) error {
var reader io.Reader = r.Body
_, direct := r.URL.Query()["direct"]
if direct {
target, err := ioutil.ReadAll(r.Body)
if err != nil {
return err
}
resp, err := http.Get(string(target))
if err != nil {
return err
}
defer resp.Body.Close()
reader = resp.Body
}
if _, err := io.Copy(f, reader); err != nil {
return err
}
return nil
}
func (rest *REST) filesDelete(w http.ResponseWriter, r *http.Request) { func (rest *REST) filesDelete(w http.ResponseWriter, r *http.Request) {
localPath := rest.filesPath(r) localPath := rest.filesPath(r)
if stat, err := os.Stat(localPath); os.IsNotExist(err) { if stat, err := os.Stat(localPath); os.IsNotExist(err) {
@@ -72,6 +97,8 @@ func (rest *REST) filesDelete(w http.ResponseWriter, r *http.Request) {
} }
func (rest *REST) filesGet(w http.ResponseWriter, r *http.Request) { func (rest *REST) filesGet(w http.ResponseWriter, r *http.Request) {
simpleserve.SetContentTypeIfMedia(w, r)
r.URL.Path = strings.TrimSuffix(r.URL.Path, path.Ext(r.URL.Path))
localPath := rest.filesPath(r) localPath := rest.filesPath(r)
if stat, err := os.Stat(localPath); os.IsNotExist(err) { if stat, err := os.Stat(localPath); os.IsNotExist(err) {
rest.respNotFound(w) rest.respNotFound(w)
@@ -106,10 +133,12 @@ func (rest *REST) filesUpdate(w http.ResponseWriter, r *http.Request) {
return return
} }
defer f.Close() defer f.Close()
if _, err := io.Copy(f, r.Body); err != nil {
if err := rest.filesStream(r, f); err != nil {
rest.respError(w, err) rest.respError(w, err)
return return
} }
if err := os.Rename(localPath+".tmp", localPath); err != nil { if err := os.Rename(localPath+".tmp", localPath); err != nil {
rest.respError(w, err) rest.respError(w, err)
return return

View File

@@ -1,6 +1,8 @@
package server package server
import ( import (
"bytes"
"fmt"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings" "strings"
@@ -11,14 +13,17 @@ import (
func TestFiles(t *testing.T) { func TestFiles(t *testing.T) {
cases := map[string]func(*testing.T, *REST, func(*http.Request)){ cases := map[string]func(*testing.T, *REST, func(*http.Request)){
"create-get": func(t *testing.T, rest *REST, scope func(r *http.Request)) { "create-get w/ content type": func(t *testing.T, rest *REST, scope func(r *http.Request)) {
content := uuid.New().String() content := uuid.New().String()
w := testFilesPost(t, rest, scope, content) w := testFilesPost(t, rest, scope, content)
if w.Code != http.StatusOK { if w.Code != http.StatusOK {
t.Fatal(w.Code, string(w.Body.Bytes())) t.Fatal(w.Code, string(w.Body.Bytes()))
} }
s := string(w.Body.Bytes()) s := fmt.Sprintf("%s.jpg", w.Body.Bytes())
w = testFilesGet(t, rest, s, scope) w = testFilesGet(t, rest, s, scope)
if w.Header().Get("Content-Type") != "image/jpeg" {
t.Fatal(w.Header())
}
if w.Code != http.StatusOK { if w.Code != http.StatusOK {
t.Fatal(w.Code, string(w.Body.Bytes())) t.Fatal(w.Code, string(w.Body.Bytes()))
} }
@@ -101,3 +106,37 @@ func testFilesReq(t *testing.T, rest *REST, id string, scope func(*http.Request)
rest.files(w, r) rest.files(w, r)
return w return w
} }
func TestFilesStream(t *testing.T) {
rest, _, clean := testREST(t)
defer clean()
t.Run("simple upload", func(t *testing.T) {
value := uuid.New().String()
r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(value))
buff := bytes.NewBuffer(nil)
if err := rest.filesStream(r, buff); err != nil {
t.Fatal(err)
}
if s := string(buff.Bytes()); s != value {
t.Fatal(s)
}
})
t.Run("direct upload", func(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(http.NotFound))
defer s.Close()
r := httptest.NewRequest(http.MethodPost, "/?direct", strings.NewReader(s.URL))
buff := bytes.NewBuffer(nil)
if err := rest.filesStream(r, buff); err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
http.NotFound(w, nil)
want := string(w.Body.Bytes())
got := string(buff.Bytes())
if want != got {
t.Fatal(want, got)
}
})
}

10
server/markdown.go Normal file
View File

@@ -0,0 +1,10 @@
package server
const (
markdownHead = `<div>
<style scoped>
@charset"UTF-8";button,input,textarea{transition:background-color 0.1s linear, border-color 0.1s linear, color 0.1s linear, box-shadow 0.1s linear, transform 0.1s ease}h1{font-size:2.2em;margin-top:0}h1,h2,h3,h4,h5,h6{margin-bottom:12px}h1,h2,h3,h4,h5,h6,strong{color:#000000}b,h1,h2,h3,h4,h5,h6,strong,th{font-weight:600}blockquote{border-left:4px solid #0096bfab;margin:1.5em 0;padding:0.5em 1em;font-style:italic}blockquote > footer{margin-top:10px;font-style:normal}blockquote cite{font-style:normal}address{font-style:normal}a[href^='mailto']::before{content:'📧 '}a[href^='tel']::before{content:'📞 '}a[href^='sms']::before{content:'💬 '}button,input[type='submit'],input[type='button'],input[type='checkbox']{cursor:pointer}input:not([type='checkbox']):not([type='radio']),select{display:block}button,input,select,textarea{color:#000000;background-color:#efefef;font-family:inherit;font-size:inherit;margin-right:6px;margin-bottom:6px;padding:10px;border:none;border-radius:6px;outline:none}input:not([type='checkbox']):not([type='radio']),select,button,textarea{-webkit-appearance:none}textarea{margin-right:0;width:100%;box-sizing:border-box;resize:vertical}button,input[type='submit'],input[type='button']{padding-right:30px;padding-left:30px}button:hover,input[type='submit']:hover,input[type='button']:hover{background:#dddddd}button:focus,input:focus,select:focus,textarea:focus{box-shadow:0 0 0 2px #0096bfab}input[type='checkbox']:active,input[type='radio']:active,input[type='submit']:active,input[type='button']:active,button:active{transform:translateY(2px)}button:disabled,input:disabled,select:disabled,textarea:disabled{cursor:not-allowed;opacity:0.5}::-webkit-input-placeholder{color:#949494}:-ms-input-placeholder{color:#949494}::-ms-input-placeholder{color:#949494}::placeholder{color:#949494}a{text-decoration:none;color:#0076d1}a:hover{text-decoration:underline}code,kbd{background:#efefef;color:#000000;padding:5px;border-radius:6px}pre > code{padding:10px;display:block;overflow-x:auto}img{max-width:100%}hr{border:none;border-top:1px solid #dbdbdb}table{border-collapse:collapse;margin-bottom:10px;width:100%}td,th{padding:6px;text-align:left}th{border-bottom:1px solid #dbdbdb}tbody tr:nth-child(even){background-color:#efefef}::-webkit-scrollbar{height:10px;width:10px}::-webkit-scrollbar-track{background:#efefef;border-radius:6px}::-webkit-scrollbar-thumb{background:#d5d5d5;border-radius:6px}::-webkit-scrollbar-thumb:hover{background:#c4c4c4}
</style>
`
markdownTail = `</div>`
)

View File

@@ -6,6 +6,7 @@ import (
"local/dndex/server/auth" "local/dndex/server/auth"
"local/gziphttp" "local/gziphttp"
"net/http" "net/http"
"path"
"strings" "strings"
"time" "time"
) )
@@ -69,3 +70,11 @@ func (rest *REST) shift(foo http.HandlerFunc) http.HandlerFunc {
foo(w, r) foo(w, r)
} }
} }
func (rest *REST) deprefix(foo http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
newpath := strings.TrimPrefix(r.URL.Path, path.Join("/", config.New().APIPrefix))
r.URL.Path = newpath
foo(w, r)
}
}

View File

@@ -131,3 +131,20 @@ func TestMiddlewareShift(t *testing.T) {
}) })
} }
} }
func TestMiddlewareDeprefix(t *testing.T) {
resolved := ""
bar := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resolved = r.URL.Path
})
os.Args = []string{"a"}
os.Setenv("API_PREFIX", "myprefix")
r := httptest.NewRequest(http.MethodGet, "/myprefix/abc", nil)
rest := &REST{}
rest.deprefix(bar)(nil, r)
if resolved != "/abc" {
t.Fatal(resolved)
}
}

View File

@@ -7,6 +7,7 @@ import (
"local/dndex/storage" "local/dndex/storage"
"local/router" "local/router"
"net/http" "net/http"
"path"
"strings" "strings"
) )
@@ -42,11 +43,11 @@ func NewREST(g storage.RateLimitedGraph) (*REST, error) {
fmt.Sprintf("dump"): rest.dump, fmt.Sprintf("dump"): rest.dump,
} }
for path, foo := range paths { for urlpath, foo := range paths {
bar := foo bar := foo
bar = rest.shift(bar) bar = rest.shift(bar)
bar = rest.scoped(bar) bar = rest.scoped(bar)
switch strings.Split(path, "/")[0] { switch strings.Split(urlpath, "/")[0] {
case "users": case "users":
case "version": case "version":
default: default:
@@ -54,11 +55,20 @@ func NewREST(g storage.RateLimitedGraph) (*REST, error) {
} }
bar = rest.defend(bar) bar = rest.defend(bar)
bar = rest.delay(bar) bar = rest.delay(bar)
if err := rest.router.Add(path, bar); err != nil { bar = rest.deprefix(bar)
routerpath := path.Join("/", config.New().APIPrefix, urlpath)
if err := rest.router.Add(routerpath, bar); err != nil {
return nil, err return nil, err
} }
} }
bar := rest.static
bar = rest.defend(bar)
bar = rest.delay(bar)
if err := rest.router.Add(params, bar); err != nil {
return nil, err
}
return rest, nil return rest, nil
} }

View File

@@ -163,10 +163,10 @@ func TestRESTRouter(t *testing.T) {
for name, d := range cases { for name, d := range cases {
c := d c := d
path := name urlpath := path.Join("/", config.New().APIPrefix, name)
rest, setAuth, clean := testREST(t) rest, setAuth, clean := testREST(t)
defer clean() defer clean()
r := httptest.NewRequest(c.method, path, strings.NewReader(``)) r := httptest.NewRequest(c.method, urlpath, strings.NewReader(``))
setAuth(r) setAuth(r)
w := httptest.NewRecorder() w := httptest.NewRecorder()
rest.router.ServeHTTP(w, r) rest.router.ServeHTTP(w, r)

18
server/static.go Normal file
View File

@@ -0,0 +1,18 @@
package server
import (
"local/dndex/config"
"local/simpleserve/simpleserve"
"net/http"
"os"
"path"
)
func (rest *REST) static(w http.ResponseWriter, r *http.Request) {
if _, err := os.Stat(path.Join(config.New().StaticRoot, r.URL.Path)); os.IsNotExist(err) {
r.URL.Path = "/"
}
simpleserve.SetContentTypeIfMedia(w, r)
server := http.FileServer(http.Dir(config.New().StaticRoot))
server.ServeHTTP(w, r)
}

46
server/static_test.go Normal file
View File

@@ -0,0 +1,46 @@
package server
import (
"io/ioutil"
"local/dndex/config"
"net/http"
"net/http/httptest"
"os"
"path"
"testing"
)
func TestRESTStatic(t *testing.T) {
os.Args = []string{"a"}
d, err := ioutil.TempDir(os.TempDir(), "static*")
if err != nil {
t.Fatal(err)
}
os.Setenv("STATIC_ROOT", d)
if err := ioutil.WriteFile(path.Join(d, "index.html"), []byte("Hello, world"), os.ModePerm); err != nil {
t.Fatal(err)
}
rest, _, clean := testREST(t)
defer clean()
t.Run("assert nonstatic OK", func(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, path.Join("/", config.New().APIPrefix, "version"), nil)
w := httptest.NewRecorder()
rest.router.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatal(w.Code)
}
})
t.Run("assert static OK", func(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/", nil)
w := httptest.NewRecorder()
rest.router.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatal(w.Code)
}
if s := string(w.Body.Bytes()); s != "Hello, world" {
t.Fatal(s)
}
})
}

View File

@@ -16,7 +16,7 @@ func (rest *REST) users(w http.ResponseWriter, r *http.Request) {
rest.respNotFound(w) rest.respNotFound(w)
return return
} }
r.Header.Set("Application-Type", "application/x-www-form-urlencoded") rest.usersContentType(r)
switch r.URL.Path { switch r.URL.Path {
case "/register": case "/register":
rest.usersRegister(w, r) rest.usersRegister(w, r)
@@ -28,6 +28,7 @@ func (rest *REST) users(w http.ResponseWriter, r *http.Request) {
} }
func (rest *REST) usersRegister(w http.ResponseWriter, r *http.Request) { func (rest *REST) usersRegister(w http.ResponseWriter, r *http.Request) {
rest.usersContentType(r)
err := auth.Register(rest.g, r) err := auth.Register(rest.g, r)
if err != nil { if err != nil {
rest.respError(w, err) rest.respError(w, err)
@@ -37,6 +38,7 @@ func (rest *REST) usersRegister(w http.ResponseWriter, r *http.Request) {
} }
func (rest *REST) usersLogin(w http.ResponseWriter, r *http.Request) { func (rest *REST) usersLogin(w http.ResponseWriter, r *http.Request) {
rest.usersContentType(r)
salt := uuid.New().String()[:5] salt := uuid.New().String()[:5]
var token string var token string
var err error var err error
@@ -56,3 +58,9 @@ func (rest *REST) usersLogin(w http.ResponseWriter, r *http.Request) {
"salt": salt, "salt": salt,
}) })
} }
func (rest *REST) usersContentType(r *http.Request) {
if r.Header.Get("Application-Type") == "" {
r.Header.Set("Application-Type", "application/x-www-form-urlencoded")
}
}

View File

@@ -17,9 +17,11 @@ func TestUsersRegister(t *testing.T) {
defer clean() defer clean()
t.Run("register ok", func(t *testing.T) { t.Run("register ok", func(t *testing.T) {
for _, json := range []bool{false, true} {
user := uuid.New().String()[:5] user := uuid.New().String()[:5]
pwd := uuid.New().String()[:5] pwd := uuid.New().String()[:5]
testRegisterOK(t, rest, user, pwd) testRegisterOK(t, rest, user, pwd, json)
}
}) })
t.Run("register 400: nil body", func(t *testing.T) { t.Run("register 400: nil body", func(t *testing.T) {
@@ -76,36 +78,54 @@ func TestUsersLogin(t *testing.T) {
defer clean() defer clean()
t.Run("login ok", func(t *testing.T) { t.Run("login ok", func(t *testing.T) {
for _, json := range []bool{false, true} {
user := uuid.New().String()[:5] user := uuid.New().String()[:5]
pwd := uuid.New().String()[:5] pwd := uuid.New().String()[:5]
testRegisterOK(t, rest, user, pwd) testRegisterOK(t, rest, user, pwd, json)
testLoginOK(t, rest, user, pwd) testLoginOK(t, rest, user, pwd, json)
}
}) })
t.Run("login 404 user", func(t *testing.T) { t.Run("login 404 user", func(t *testing.T) {
for _, json := range []bool{false, true} {
pwd := uuid.New().String()[:5] pwd := uuid.New().String()[:5]
testLoginNotOK(t, rest, "bad", pwd) testLoginNotOK(t, rest, "bad", pwd, json)
}
}) })
t.Run("login bad user", func(t *testing.T) { t.Run("login bad user", func(t *testing.T) {
for _, json := range []bool{false, true} {
user := uuid.New().String()[:5] user := uuid.New().String()[:5]
pwd := uuid.New().String()[:5] pwd := uuid.New().String()[:5]
testRegisterOK(t, rest, user, pwd) testRegisterOK(t, rest, user, pwd, json)
testLoginNotOK(t, rest, "bad", pwd) testLoginNotOK(t, rest, "bad", pwd, json)
}
}) })
t.Run("login bad pwd", func(t *testing.T) { t.Run("login bad pwd", func(t *testing.T) {
for _, json := range []bool{false, true} {
user := uuid.New().String()[:5] user := uuid.New().String()[:5]
pwd := uuid.New().String()[:5] pwd := uuid.New().String()[:5]
testRegisterOK(t, rest, user, pwd) testRegisterOK(t, rest, user, pwd, json)
testLoginNotOK(t, rest, user, "bad") testLoginNotOK(t, rest, user, "bad", json)
}
}) })
} }
func testRegisterOK(t *testing.T, rest *REST, user, pwd string) { func testRegisterOK(t *testing.T, rest *REST, user, pwd string, useJSON bool) {
body := fmt.Sprintf(`%s=%s&%s=%s`, auth.UserKey, user, auth.AuthKey, pwd) body := fmt.Sprintf(`%s=%s&%s=%s`, auth.UserKey, user, auth.AuthKey, pwd)
if useJSON {
s, _ := json.Marshal(map[string]string{
auth.UserKey: user,
auth.AuthKey: pwd,
})
body = string(s)
}
r := httptest.NewRequest(http.MethodPost, "/register", strings.NewReader(body)) r := httptest.NewRequest(http.MethodPost, "/register", strings.NewReader(body))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded") r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
if useJSON {
r.Header.Set("Content-Type", "application/json")
}
w := httptest.NewRecorder() w := httptest.NewRecorder()
rest.users(w, r) rest.users(w, r)
if w.Code != http.StatusOK { if w.Code != http.StatusOK {
@@ -113,10 +133,19 @@ func testRegisterOK(t *testing.T, rest *REST, user, pwd string) {
} }
} }
func testLoginNotOK(t *testing.T, rest *REST, user, pwd string) { func testLoginNotOK(t *testing.T, rest *REST, user, pwd string, useJSON bool) {
body := fmt.Sprintf(`%s=%s`, auth.UserKey, user) body := fmt.Sprintf(`%s=%s`, auth.UserKey, user)
if useJSON {
s, _ := json.Marshal(map[string]string{
auth.UserKey: user,
})
body = string(s)
}
r := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(body)) r := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(body))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded") r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
if useJSON {
r.Header.Set("Content-Type", "application/json")
}
w := httptest.NewRecorder() w := httptest.NewRecorder()
rest.users(w, r) rest.users(w, r)
if w.Code < http.StatusBadRequest { if w.Code < http.StatusBadRequest {
@@ -136,10 +165,19 @@ func testLoginNotOK(t *testing.T, rest *REST, user, pwd string) {
} }
} }
func testLoginOK(t *testing.T, rest *REST, user, pwd string) string { func testLoginOK(t *testing.T, rest *REST, user, pwd string, useJSON bool) string {
body := fmt.Sprintf(`%s=%s`, auth.UserKey, user) body := fmt.Sprintf(`%s=%s`, auth.UserKey, user)
if useJSON {
s, _ := json.Marshal(map[string]string{
auth.UserKey: user,
})
body = string(s)
}
r := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(body)) r := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(body))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded") r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
if useJSON {
r.Header.Set("Content-Type", "application/json")
}
w := httptest.NewRecorder() w := httptest.NewRecorder()
rest.users(w, r) rest.users(w, r)
if w.Code != http.StatusOK { if w.Code != http.StatusOK {

View File

@@ -2,6 +2,7 @@ package driver
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"local/dndex/storage/entity" "local/dndex/storage/entity"
"local/storage" "local/storage"
@@ -63,7 +64,7 @@ func (s *Storage) Update(ctx context.Context, ns string, filter, operator interf
if err != nil { if err != nil {
return err return err
} }
v, err = bson.Marshal(n) v, err = json.Marshal(n)
if err != nil { if err != nil {
return err return err
} }
@@ -72,12 +73,12 @@ func (s *Storage) Update(ctx context.Context, ns string, filter, operator interf
} }
func (s *Storage) Insert(ctx context.Context, ns string, doc interface{}) error { func (s *Storage) Insert(ctx context.Context, ns string, doc interface{}) error {
b, err := bson.Marshal(doc) b, err := json.Marshal(doc)
if err != nil { if err != nil {
return err return err
} }
m := bson.M{} m := bson.M{}
if err := bson.Unmarshal(b, &m); err != nil { if err := json.Unmarshal(b, &m); err != nil {
return err return err
} }
@@ -122,11 +123,15 @@ func (s *Storage) forEach(ctx context.Context, ns string, filter interface{}, fo
return err return err
} else { } else {
n := bson.M{} n := bson.M{}
if err := bson.Unmarshal(v, &n); err != nil { if err := json.Unmarshal(v, &n); err != nil {
return err return err
} }
if matches(n, m) { if matches(n, m) {
if err := foo(id, append(bson.Raw{}, bson.Raw(v)...)); err != nil { b, err := bson.Marshal(n)
if err != nil {
return err
}
if err := foo(id, b); err != nil {
return err return err
} }
} }

View File

@@ -1,12 +1,12 @@
package driver package driver
import ( import (
"encoding/json"
"local/dndex/storage/entity" "local/dndex/storage/entity"
"testing" "testing"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"go.mongodb.org/mongo-driver/bson"
) )
func TestNewStorage(t *testing.T) { func TestNewStorage(t *testing.T) {
@@ -37,7 +37,7 @@ func fillStorage(t *testing.T, s *Storage) {
Connections: map[string]entity.Connection{p.ID: entity.Connection{p.Name}}, Connections: map[string]entity.Connection{p.ID: entity.Connection{p.Name}},
Attachments: map[string]entity.Attachment{"filename": {"/path/to/file"}}, Attachments: map[string]entity.Attachment{"filename": {"/path/to/file"}},
} }
b, err := bson.Marshal(o) b, err := json.Marshal(o)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@@ -3,7 +3,6 @@ package entity
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"strings"
"time" "time"
"go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson"
@@ -62,6 +61,18 @@ func (o One) Generic() bson.M {
return m return m
} }
func (o One) MarshalJSON() ([]byte, error) {
b, err := o.MarshalBSON()
if err != nil {
return nil, err
}
v := bson.M{}
if err := bson.Unmarshal(b, &v); err != nil {
return nil, err
}
return json.Marshal(v)
}
func (o One) MarshalBSON() ([]byte, error) { func (o One) MarshalBSON() ([]byte, error) {
isMin := fmt.Sprint(o) == fmt.Sprint(o.Query()) isMin := fmt.Sprint(o) == fmt.Sprint(o.Query())
if !isMin { if !isMin {
@@ -73,19 +84,39 @@ func (o One) MarshalBSON() ([]byte, error) {
if o.Attachments == nil { if o.Attachments == nil {
o.Attachments = make(map[string]Attachment) o.Attachments = make(map[string]Attachment)
} }
b, err := json.Marshal(o) var v interface{}
b, err := o.toBytes()
if err != nil { if err != nil {
return nil, err return nil, err
} }
m := bson.M{} if err := fromBytes(b, &v); err != nil {
if err := json.Unmarshal(b, &m); err != nil {
return nil, err return nil, err
} }
for k, v := range m { return bson.Marshal(v)
switch v.(type) {
case string:
m[k] = strings.TrimSpace(v.(string))
} }
func (o One) toBytes() ([]byte, error) {
return json.Marshal(struct {
ID string `bson:"_id,omitempty" json:"_id"`
Name string `bson:"name,omitempty" json:"name"`
Type string `bson:"type,omitempty" json:"type"`
Title string `bson:"title,omitempty" json:"title"`
Text string `bson:"text,omitempty" json:"text"`
Modified int64 `bson:"modified,omitempty" json:"modified"`
Connections map[string]Connection `bson:"connections" json:"connections"`
Attachments map[string]Attachment `bson:"attachments" json:"attachments"`
}{
ID: o.ID,
Name: o.Name,
Type: o.Type,
Title: o.Title,
Text: o.Text,
Modified: o.Modified,
Connections: o.Connections,
Attachments: o.Attachments,
})
} }
return bson.Marshal(m)
func fromBytes(b []byte, v interface{}) error {
return json.Unmarshal(b, v)
} }

View File

@@ -57,3 +57,24 @@ func TestOneMarshalBSON(t *testing.T) {
}) })
} }
} }
func TestToFromBytes(t *testing.T) {
o := One{
Name: "hello",
Connections: map[string]Connection{
"world": Connection{Relationship: "!"},
},
}
b, err := o.toBytes()
if err != nil {
t.Fatal(err)
}
var v interface{}
if err := fromBytes(b, &v); err != nil {
t.Fatal(err)
}
t.Log(v)
}