parent
5eb73b41e7
commit
1030e9c614
@ -0,0 +1,21 @@
|
||||
package internal
|
||||
|
||||
type Bookmark struct {
|
||||
ID string
|
||||
URL string
|
||||
Name string
|
||||
Tags []string
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID uint64
|
||||
Username string
|
||||
PasswordHash string
|
||||
}
|
||||
|
||||
type Storage interface {
|
||||
AddUser(User) (uint64, error)
|
||||
AddBookmark(string, string, []string) error
|
||||
GetBookmarks(string) ([]Bookmark, error)
|
||||
DeleteBookmark(string) error
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
)
|
||||
|
||||
func AccessLoggerMiddleware(middleware http.Handler) http.Handler {
|
||||
accessLogger := log.New(os.Stdout, "[access] ", log.Ldate|log.Lmicroseconds|log.Lshortfile)
|
||||
|
||||
return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||
requestId := uuid.Must(uuid.NewV4())
|
||||
setupHeaders(res.Header(), requestId.String())
|
||||
accessLogger.Printf("[%s] %s %s '%s'", requestId, getRemoteIP(req), req.Method, req.URL.String())
|
||||
middleware.ServeHTTP(res, req.WithContext(context.WithValue(req.Context(), "id", requestId.String())))
|
||||
})
|
||||
}
|
||||
|
||||
func setupHeaders(headers http.Header, requestId string) {
|
||||
headers.Set("X-RiffRaff-Request-Id", requestId)
|
||||
headers.Set("Server", "Riff Raff")
|
||||
headers.Set("Cache-Control", fmt.Sprintf("public, max-age=%s", time.Hour/time.Second))
|
||||
}
|
||||
|
||||
func getRemoteIP(req *http.Request) string {
|
||||
ip := req.RemoteAddr
|
||||
h := req.Header
|
||||
|
||||
if realIpHeader := h.Get("X-Real-Ip"); realIpHeader != "" {
|
||||
ip = realIpHeader
|
||||
}
|
||||
|
||||
if forwardIpHeader := h.Get("X-Forwarded-For"); forwardIpHeader != "" {
|
||||
ip = forwardIpHeader
|
||||
}
|
||||
|
||||
return ip
|
||||
}
|
@ -1,120 +0,0 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultSearchProvider = "https://duckduckgo.com/%s"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnrecognizedCommand = errors.New("unrecognized command")
|
||||
ErrNotEnoughArguments = errors.New("not enough arguments")
|
||||
)
|
||||
|
||||
type CommandHandler struct {
|
||||
*sync.Mutex
|
||||
Shortcuts map[string]string
|
||||
}
|
||||
|
||||
func (c *CommandHandler) Handle(input string) (Command, error) {
|
||||
rawArgs := strings.Fields(input)
|
||||
fragmentCount := len(rawArgs)
|
||||
farg := "*"
|
||||
|
||||
var shortcut string
|
||||
|
||||
if fragmentCount > 0 {
|
||||
farg = rawArgs[0]
|
||||
|
||||
if fragmentCount > 1 {
|
||||
shortcut = rawArgs[1]
|
||||
}
|
||||
|
||||
var updateShortcutParams string
|
||||
if fragmentCount > 2 {
|
||||
updateShortcutParams = strings.Join(rawArgs[2:], " ")
|
||||
}
|
||||
|
||||
if cmd, err := c.updateShortcut(farg, shortcut, updateShortcutParams); err != ErrUnrecognizedCommand {
|
||||
return cmd, err
|
||||
}
|
||||
}
|
||||
|
||||
return c.getShortcut(farg, rawArgs...), nil
|
||||
}
|
||||
|
||||
func (c *CommandHandler) updateShortcut(action, shortcut, location string) (Command, error) {
|
||||
command := Command{
|
||||
Action: action,
|
||||
Name: shortcut,
|
||||
Location: location,
|
||||
}
|
||||
|
||||
switch action {
|
||||
case "add":
|
||||
if location == "" {
|
||||
return Command{}, ErrNotEnoughArguments
|
||||
}
|
||||
|
||||
c.Lock()
|
||||
c.Shortcuts[shortcut] = command.Location
|
||||
c.Unlock()
|
||||
|
||||
case "remove":
|
||||
if shortcut == "" {
|
||||
return Command{}, ErrNotEnoughArguments
|
||||
}
|
||||
|
||||
command.Location = c.Shortcuts[shortcut]
|
||||
|
||||
c.Lock()
|
||||
delete(c.Shortcuts, shortcut)
|
||||
c.Unlock()
|
||||
|
||||
default:
|
||||
return Command{}, ErrUnrecognizedCommand
|
||||
}
|
||||
|
||||
return command, nil
|
||||
}
|
||||
|
||||
func (c *CommandHandler) getShortcut(key string, input ...string) Command {
|
||||
var parameter string
|
||||
|
||||
location, ok := c.Shortcuts[key]
|
||||
if !ok {
|
||||
location = DefaultSearchProvider
|
||||
key = "*"
|
||||
parameter = strings.Join(input, " ")
|
||||
} else {
|
||||
parameter = strings.Join(input[1:], " ")
|
||||
}
|
||||
|
||||
if strings.Contains(location, "%s") {
|
||||
location = fmt.Sprintf(location, parameter)
|
||||
}
|
||||
|
||||
return Command{
|
||||
Action: "lookup",
|
||||
Name: key,
|
||||
Location: location,
|
||||
}
|
||||
}
|
||||
|
||||
func NewDefaultShortcuts() Shortcuts {
|
||||
return Shortcuts{
|
||||
"*": DefaultSearchProvider,
|
||||
"help": "/",
|
||||
}
|
||||
}
|
||||
|
||||
type Command struct {
|
||||
Action string
|
||||
Name string
|
||||
Location string
|
||||
}
|
@ -1,113 +0,0 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_Handle(t *testing.T) {
|
||||
testHandleCases := []struct {
|
||||
name string
|
||||
input string
|
||||
want Command
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty input should send to default search provider",
|
||||
input: "",
|
||||
want: Command{
|
||||
Action: "lookup",
|
||||
Name: "*",
|
||||
Location: fmt.Sprintf(DefaultSearchProvider, ""),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "add shortcut: 'add gh https://github.com'",
|
||||
input: "add gh https://github.com",
|
||||
want: Command{
|
||||
Action: "add",
|
||||
Name: "gh",
|
||||
Location: "https://github.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "add shortcut without location: 'add gh'",
|
||||
input: "add gh",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "add search shortcut: 'add so https://stackvoerflow.com?q=%s'",
|
||||
input: "add so https://stackoverflow.com?q=%s",
|
||||
want: Command{
|
||||
Action: "add",
|
||||
Name: "so",
|
||||
Location: "https://stackoverflow.com?q=%s",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remove shortcut: 'remove gh'",
|
||||
input: "remove gh",
|
||||
want: Command{
|
||||
Action: "remove",
|
||||
Name: "gh",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remove command with incorrect number of args: 'remove'",
|
||||
input: "remove",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "forward search to search provider: 'golang unit testing frameworks'",
|
||||
input: "golang unit testing frameworks",
|
||||
want: Command{
|
||||
Action: "lookup",
|
||||
Name: "*",
|
||||
Location: fmt.Sprintf(DefaultSearchProvider, "golang unit testing frameworks"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "visit a shortcut: 'fb'",
|
||||
input: "fb",
|
||||
want: Command{
|
||||
Action: "lookup",
|
||||
Name: "fb",
|
||||
Location: "https://facebook.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "visit a search shortcut: 'go net/http'",
|
||||
input: "go net/http",
|
||||
want: Command{
|
||||
Action: "lookup",
|
||||
Name: "go",
|
||||
Location: "https://godoc.org/net/http",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testHandleCases {
|
||||
tt := testCase
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cm := &CommandHandler{
|
||||
Mutex: &sync.Mutex{},
|
||||
Shortcuts: map[string]string{
|
||||
"fb": "https://facebook.com",
|
||||
"go": "https://godoc.org/%s",
|
||||
},
|
||||
}
|
||||
|
||||
got, err := cm.Handle(tt.input)
|
||||
|
||||
if tt.wantErr != (err != nil) {
|
||||
t.Errorf("wantErr: %v got '%v'", tt.wantErr, err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("parseInput('%s') = %v, want %v", tt.input, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,120 +0,0 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
type ShortcutStore struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
type ShortcutData struct {
|
||||
Shortcuts Shortcuts `json:"shortcuts"`
|
||||
}
|
||||
|
||||
func (ss *ShortcutStore) Init() error {
|
||||
defaultSS := NewDefaultShortcuts()
|
||||
|
||||
if _, err := os.Stat(ss.Path); os.IsNotExist(err) {
|
||||
log.Printf("file doesn't exist %s: %v", ss.Path, err)
|
||||
|
||||
if err := ss.SaveShortcuts(defaultSS, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := ss.LoadShortcuts(nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ss *ShortcutStore) SaveShortcuts(shorts Shortcuts, copyTo io.Writer) error {
|
||||
file, err := os.Create(ss.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
temp := Shortcuts{}
|
||||
|
||||
for k, v := range shorts {
|
||||
if k != "help" {
|
||||
temp[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
sc := ShortcutData{Shortcuts: temp}
|
||||
|
||||
var target io.Writer
|
||||
if copyTo != nil {
|
||||
target = io.MultiWriter(file, copyTo)
|
||||
} else {
|
||||
target = file
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(target)
|
||||
encoder.SetIndent("", "\t")
|
||||
|
||||
if err := encoder.Encode(&sc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ss *ShortcutStore) LoadShortcuts(copyTo io.Writer) (Shortcuts, error) {
|
||||
var sc ShortcutData
|
||||
|
||||
file, err := os.Open(ss.Path)
|
||||
if err != nil {
|
||||
if !os.IsExist(err) {
|
||||
return NewDefaultShortcuts(), nil
|
||||
}
|
||||
|
||||
return sc.Shortcuts, err
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
finfo, err := file.Stat()
|
||||
if err != nil {
|
||||
return sc.Shortcuts, err
|
||||
}
|
||||
|
||||
if finfo.Size() == 0 {
|
||||
return NewDefaultShortcuts(), nil
|
||||
}
|
||||
|
||||
var target io.Reader
|
||||
|
||||
if copyTo != nil {
|
||||
target = io.TeeReader(file, copyTo)
|
||||
} else {
|
||||
target = file
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(target)
|
||||
if err := decoder.Decode(&sc); err != nil {
|
||||
return sc.Shortcuts, err
|
||||
}
|
||||
|
||||
if sc.Shortcuts == nil {
|
||||
return NewDefaultShortcuts(), nil
|
||||
}
|
||||
|
||||
sc.Shortcuts["help"] = "/"
|
||||
|
||||
if v, ok := sc.Shortcuts["*"]; !ok || v == "" {
|
||||
sc.Shortcuts["*"] = DefaultSearchProvider
|
||||
}
|
||||
|
||||
return sc.Shortcuts, nil
|
||||
}
|
@ -1,87 +1,12 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"html/template"
|
||||
)
|
||||
import "net/http"
|
||||
|
||||
type TemplateRenderer struct {
|
||||
FS http.FileSystem
|
||||
}
|
||||
|
||||
type TemplateVariables struct {
|
||||
Host string
|
||||
Entries map[string]string
|
||||
http.FileSystem
|
||||
}
|
||||
|
||||
func ToTemplateVars(req *http.Request, shortcuts map[string]string) TemplateVariables {
|
||||
scheme := req.URL.Scheme
|
||||
host := req.URL.Host
|
||||
|
||||
if scheme == "" {
|
||||
scheme = "http"
|
||||
}
|
||||
|
||||
if host == "" {
|
||||
if host = req.Host; host == "" {
|
||||
host = req.Header.Get("X-Forwarded-For")
|
||||
}
|
||||
}
|
||||
|
||||
fqdn := fmt.Sprintf("%s://%s", scheme, host)
|
||||
|
||||
return TemplateVariables{
|
||||
Host: fqdn,
|
||||
Entries: shortcuts,
|
||||
}
|
||||
}
|
||||
|
||||
func (tr TemplateRenderer) Render(name string, templVars TemplateVariables, w io.Writer) error {
|
||||
templateFileName := fmt.Sprintf("%s.tpl", name)
|
||||
|
||||
f, err := tr.FS.Open(templateFileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
templateBytes, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
templateStr := string(templateBytes)
|
||||
|
||||
templateObj, parseTemplateErr := template.New(templateFileName).Parse(templateStr)
|
||||
if parseTemplateErr != nil {
|
||||
return fmt.Errorf("could not parse template for '%s': %w", templateFileName, parseTemplateErr)
|
||||
}
|
||||
|
||||
log.Printf("%+v", templVars)
|
||||
|
||||
return templateObj.Execute(w, templVars)
|
||||
}
|
||||
|
||||
func (tr TemplateRenderer) RenderHandler(filename string, ss *CommandHandler, headers http.Header) http.HandlerFunc {
|
||||
return func(res http.ResponseWriter, req *http.Request) {
|
||||
templVar := ToTemplateVars(req, ss.Shortcuts)
|
||||
h := res.Header()
|
||||
|
||||
for k, v := range headers {
|
||||
h[k] = v
|
||||
}
|
||||
|
||||
h.Set("Cache-Control", "max-age 0; no-cache; private")
|
||||
|
||||
if err := tr.Render(filename, templVar, res); err != nil {
|
||||
http.Error(res, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
func (tp TemplateRenderer) Handle(templateFile string, model interface{}) http.Handler {
|
||||
return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in new issue