pull/9/head
Adam Veldhousen 4 years ago
parent 5eb73b41e7
commit 1030e9c614

@ -13,7 +13,7 @@
A browser omni search bar enhancer. Side effects may include enhanced productivity, happiness and less google search tracking.
## How to install
## How to use
- Install it:
```

@ -4,6 +4,7 @@ go 1.13
require (
github.com/gobuffalo/packr v1.30.1
github.com/gofrs/uuid v3.2.0+incompatible
github.com/golangci/golangci-lint v1.21.0 // indirect
github.com/satori/go.uuid v1.2.0
)

@ -75,6 +75,8 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gofrs/flock v0.0.0-20190320160742-5135e617513b h1:ekuhfTjngPhisSjOJ0QWKpPQE8/rbknHaes6WVJj5Hw=
github.com/gofrs/flock v0.0.0-20190320160742-5135e617513b/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
@ -254,6 +256,7 @@ github.com/valyala/quicktemplate v1.2.0/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOV
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=

@ -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
}

@ -1,15 +1,8 @@
package internal
import (
"context"
"fmt"
"log"
"net/http"
"os"
"sync"
"time"
uuid "github.com/satori/go.uuid"
)
type Shortcuts map[string]string
@ -33,75 +26,5 @@ func NewServer(tp TemplateRenderer, shortcutStore *ShortcutStore, accessLogging
mux.HandleFunc("/search", handlerFunc)
mux.HandleFunc("/search_to_home", handlerFunc)
return accessLoggerMiddleware(mux)
}
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.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 searchHandler(scs *CommandHandler, shortcutStore *ShortcutStore, logAccess bool) http.HandlerFunc {
searchLogger := log.New(os.Stdout, "[search] ", log.Ldate|log.Lmicroseconds|log.Lshortfile)
return func(res http.ResponseWriter, req *http.Request) {
requestId := req.Context().Value("id")
v := req.URL.Query()
commandString := v.Get("q")
if commandString == "" {
http.Redirect(res, req, "https://duckduckgo.com", http.StatusFound)
return
}
action, err := scs.Handle(commandString)
if err != nil {
if logAccess {
searchLogger.Printf("[%s] '%s' -> got error: '%s'", requestId, commandString, err.Error())
}
http.Redirect(res, req, fmt.Sprintf("https://duckduckgo.com?q=%s", commandString), http.StatusFound)
return
}
searchLogger.Printf("[%s] '%s' %s -> 302 %s", requestId, commandString, action.Action, action.Location)
if action.Action != "lookup" {
if err := shortcutStore.SaveShortcuts(scs.Shortcuts, nil); err != nil {
if logAccess {
searchLogger.Printf("[%s] '%s' %s -> could not save shortcuts database file: %v", requestId, commandString, action.Action, err)
}
http.Error(res, err.Error(), http.StatusInternalServerError)
return
}
}
http.Redirect(res, req, action.Location, http.StatusFound)
}
}
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
return AccessLoggerMiddleware(mux)
}

@ -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) {
})
}

@ -13,7 +13,7 @@ import (
func main() {
port := flag.Int("port", 80, "port to listen on")
bindAddr := flag.String("bind", "127.0.0.1", "interface to bind to")
dbPath := flag.String("data", "./data.json", "path to save shortcut database")
dbPath := flag.String("data", "./data", "path to save shortcut database")
enableAccessLogging := flag.Bool("accesslog", true, "Enable access logging")
flag.Parse()

@ -37,6 +37,9 @@ lint: $(LINTBIN)
$(LINTBIN):
@GO111MODULE=on go get github.com/golangci/golangci-lint/cmd/golangci-lint
$(TMPDIR):
mkdir $(TMPDIR)
package:
@docker build --build-arg VERSION=$(VERSION) \
--build-arg COMMIT=$(GIT_SHA) \

Loading…
Cancel
Save