Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for running in read-only mode #119

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
30 changes: 22 additions & 8 deletions golink.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ var (
hostname = flag.String("hostname", defaultHostname, "service name")
resolveFromBackup = flag.String("resolve-from-backup", "", "resolve a link from snapshot file and exit")
allowUnknownUsers = flag.Bool("allow-unknown-users", false, "allow unknown users to save links")
readonly = flag.Bool("readonly", false, "start golink server in read-only mode")
)

var stats struct {
Expand Down Expand Up @@ -269,10 +270,11 @@ type visitData struct {

// homeData is the data used by homeTmpl.
type homeData struct {
Short string
Long string
Clicks []visitData
XSRF string
Short string
Long string
Clicks []visitData
XSRF string
ReadOnly bool
}

// deleteData is the data used by deleteTmpl.
Expand Down Expand Up @@ -443,10 +445,11 @@ func serveHome(w http.ResponseWriter, r *http.Request, short string) {
return
}
homeTmpl.Execute(w, homeData{
Short: short,
Long: long,
Clicks: clicks,
XSRF: xsrftoken.Generate(xsrfKey, cu.login, newShortName),
Short: short,
Long: long,
Clicks: clicks,
XSRF: xsrftoken.Generate(xsrfKey, cu.login, newShortName),
ReadOnly: *readonly,
})
}

Expand Down Expand Up @@ -743,6 +746,10 @@ func userExists(ctx context.Context, login string) (bool, error) {
var reShortName = regexp.MustCompile(`^\w[\w\-\.]*$`)

func serveDelete(w http.ResponseWriter, r *http.Request) {
if *readonly {
http.Error(w, "golink is in read-only mode", http.StatusMethodNotAllowed)
return
}
short := strings.TrimPrefix(r.URL.Path, "/.delete/")
if short == "" {
http.Error(w, "short required", http.StatusBadRequest)
Expand Down Expand Up @@ -793,6 +800,10 @@ func serveDelete(w http.ResponseWriter, r *http.Request) {
// long URL are validated for proper format. Existing links may only be updated
// by their owner.
func serveSave(w http.ResponseWriter, r *http.Request) {
if *readonly {
http.Error(w, "golink is in read-only mode", http.StatusMethodNotAllowed)
return
}
short, long := r.FormValue("short"), r.FormValue("long")
if short == "" || long == "" {
http.Error(w, "short and long required", http.StatusBadRequest)
Expand Down Expand Up @@ -871,6 +882,9 @@ func serveSave(w http.ResponseWriter, r *http.Request) {
// Admin users can edit all links.
// Non-admin users can only edit their own links or links without an active owner.
func canEditLink(ctx context.Context, link *Link, u user) bool {
if *readonly {
return false
}
if link == nil || link.Owner == "" {
// new or unowned link
return true
Expand Down
42 changes: 42 additions & 0 deletions golink_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"time"

"golang.org/x/net/xsrftoken"
"tailscale.com/types/ptr"
"tailscale.com/util/must"
)

Expand Down Expand Up @@ -332,6 +333,47 @@ func TestServeDelete(t *testing.T) {
}
}

func TestReadOnlyMode(t *testing.T) {
var err error
db, err = NewSQLiteDB(":memory:")
if err != nil {
t.Fatal(err)
}
db.Save(&Link{Short: "who", Long: "http://who/"})

oldReadOnly := readonly
readonly = ptr.To(true)
defer func() { readonly = oldReadOnly }()

// resolving link should succeed
r := httptest.NewRequest("GET", "/who", nil)
w := httptest.NewRecorder()
serveHandler().ServeHTTP(w, r)
if want := http.StatusFound; w.Code != want {
t.Errorf("serveHandler() = %d; want %d", w.Code, want)
}
wantLocation := "http://who/"
if location := w.Header().Get("Location"); location != wantLocation {
t.Errorf("serveHandler() location = %v; want %v", location, wantLocation)
}

// updating link should fail
r = httptest.NewRequest("POST", "/", nil)
w = httptest.NewRecorder()
serveHandler().ServeHTTP(w, r)
if want := http.StatusMethodNotAllowed; w.Code != want {
t.Errorf("serveHandler() = %d; want %d", w.Code, want)
}

// deleting link should fail
r = httptest.NewRequest("POST", "/.delete/who", nil)
w = httptest.NewRecorder()
serveHandler().ServeHTTP(w, r)
if want := http.StatusMethodNotAllowed; w.Code != want {
t.Errorf("serveHandler() = %d; want %d", w.Code, want)
}
}

func TestExpandLink(t *testing.T) {
tests := []struct {
name string // test name
Expand Down
15 changes: 15 additions & 0 deletions static/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -1351,6 +1351,11 @@ select {
border-color: rgb(178 45 48 / var(--tw-border-opacity));
}

.border-orange-50 {
--tw-border-opacity: 1;
border-color: rgb(254 227 192 / var(--tw-border-opacity));
}

.bg-gray-100 {
--tw-bg-opacity: 1;
background-color: rgb(247 245 244 / var(--tw-bg-opacity));
Expand All @@ -1366,6 +1371,11 @@ select {
background-color: rgb(178 45 48 / var(--tw-bg-opacity));
}

.bg-orange-0 {
--tw-bg-opacity: 1;
background-color: rgb(255 250 238 / var(--tw-bg-opacity));
}

.p-2 {
padding: 0.5rem;
}
Expand All @@ -1390,6 +1400,11 @@ select {
padding-bottom: 1rem;
}

.py-3 {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
}

.pt-6 {
padding-top: 1.5rem;
}
Expand Down
32 changes: 21 additions & 11 deletions tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,26 +41,36 @@ module.exports = {
800: "rgba(90, 0, 0)",
900: "rgba(66, 0, 0)",
},
white: '#fff',
current: 'currentColor',
orange: {
0: "rgba(255, 250, 238)",
50: "rgba(254, 227, 192)",
100: "rgba(248, 184, 134)",
200: "rgba(245, 146, 94)",
300: "rgba(229, 111, 74)",
400: "rgba(196, 76, 52)",
500: "rgba(158, 47, 40)",
600: "rgba(126, 30, 35)",
700: "rgba(93, 22, 27)",
800: "rgba(66, 14, 17)",
900: "rgba(66, 14, 17)",
},
white: "#fff",
current: "currentColor",
},
extend: {
typography: {
DEFAULT: {
css: {
'code::before': {
'content': '',
"code::before": {
content: "",
},
'code::after': {
'content': '',
"code::after": {
content: "",
},
},
},
},
},
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
],
}
plugins: [require("@tailwindcss/forms"), require("@tailwindcss/typography")],
};
34 changes: 19 additions & 15 deletions tmpl/home.html
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
{{ define "main" }}
<h2 class="text-xl font-bold pb-2">Create a new link</h2>
{{ if .ReadOnly }}
<p class="rounded-md py-3 px-4 bg-orange-0 border border-orange-50">golink is running in read-only mode. Links can be resolved, but not created or updated.</p>
{{ else }}
<h2 class="text-xl font-bold pb-2">Create a new link</h2>

{{ with .Long }}
<p class="">Did you mean <a class="text-blue-600 hover:underline" href="{{.}}">{{.}}</a> ? Create a go link for it now:</p>
{{ with .Long }}
<p class="">Did you mean <a class="text-blue-600 hover:underline" href="{{.}}">{{.}}</a> ? Create a go link for it now:</p>
{{ end }}
<form method="POST" action="/" class="flex flex-wrap">
<input type="hidden" name="xsrf" value="{{ .XSRF }}" />
<div class="flex">
<label for=short class="flex my-2 px-2 items-center bg-gray-100 border border-r-0 border-gray-300 rounded-l-md text-gray-700">http://go/</label>
<input id=short name=short required type=text size=15 placeholder="shortname" value="{{.Short}}" pattern="\w[\w\-\.]*" title="Must start with letter or number; may contain letters, numbers, dashes, and periods."
class="p-2 my-2 rounded-r-md border-gray-300 placeholder:text-gray-400">
<span class="flex m-2 items-center">&rarr;</span>
</div>
<input name=long required type=text size=40 placeholder="https://destination-url"{{if .Short}} value="{{.Long}}" autofocus{{end}} class="p-2 my-2 mr-2 max-w-full rounded-md border-gray-300 placeholder:text-gray-400">
<button type=submit class="py-2 px-4 my-2 rounded-md bg-blue-500 border-blue-500 text-white hover:bg-blue-600 hover:border-blue-600">Create</button>
</form>
<p class="text-sm text-gray-500"><a class="text-blue-600 hover:underline" href="/.help">Help and advanced options</a></p>
{{ end }}
<form method="POST" action="/" class="flex flex-wrap">
<input type="hidden" name="xsrf" value="{{ .XSRF }}" />
<div class="flex">
<label for=short class="flex my-2 px-2 items-center bg-gray-100 border border-r-0 border-gray-300 rounded-l-md text-gray-700">http://go/</label>
<input id=short name=short required type=text size=15 placeholder="shortname" value="{{.Short}}" pattern="\w[\w\-\.]*" title="Must start with letter or number; may contain letters, numbers, dashes, and periods."
class="p-2 my-2 rounded-r-md border-gray-300 placeholder:text-gray-400">
<span class="flex m-2 items-center">&rarr;</span>
</div>
<input name=long required type=text size=40 placeholder="https://destination-url"{{if .Short}} value="{{.Long}}" autofocus{{end}} class="p-2 my-2 mr-2 max-w-full rounded-md border-gray-300 placeholder:text-gray-400">
<button type=submit class="py-2 px-4 my-2 rounded-md bg-blue-500 border-blue-500 text-white hover:bg-blue-600 hover:border-blue-600">Create</button>
</form>
<p class="text-sm text-gray-500"><a class="text-blue-600 hover:underline" href="/.help">Help and advanced options</a></p>

<h2 class="text-xl font-bold pt-6 pb-2">Popular Links</h2>
<table class="table-auto ">
Expand Down