Skip to content

Commit fd79f51

Browse files
committed
add support for running in read-only mode
Right now, running in read-only mode displays a banner stating that links can be resolved but not created or updated, as well as actively blocking requests to modify links. Update #118 Signed-off-by: Will Norris <[email protected]>
1 parent ecf5e8f commit fd79f51

File tree

5 files changed

+119
-34
lines changed

5 files changed

+119
-34
lines changed

golink.go

+22-8
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ var (
6565
configDir = flag.String("config-dir", "", `tsnet configuration directory ("" to use default)`)
6666
resolveFromBackup = flag.String("resolve-from-backup", "", "resolve a link from snapshot file and exit")
6767
allowUnknownUsers = flag.Bool("allow-unknown-users", false, "allow unknown users to save links")
68+
readonly = flag.Bool("readonly", false, "start golink server in read-only mode")
6869
)
6970

7071
var stats struct {
@@ -271,10 +272,11 @@ type visitData struct {
271272

272273
// homeData is the data used by homeTmpl.
273274
type homeData struct {
274-
Short string
275-
Long string
276-
Clicks []visitData
277-
XSRF string
275+
Short string
276+
Long string
277+
Clicks []visitData
278+
XSRF string
279+
ReadOnly bool
278280
}
279281

280282
// deleteData is the data used by deleteTmpl.
@@ -479,10 +481,11 @@ func serveHome(w http.ResponseWriter, r *http.Request, short string) {
479481
return
480482
}
481483
homeTmpl.Execute(w, homeData{
482-
Short: short,
483-
Long: long,
484-
Clicks: clicks,
485-
XSRF: xsrftoken.Generate(xsrfKey, cu.login, newShortName),
484+
Short: short,
485+
Long: long,
486+
Clicks: clicks,
487+
XSRF: xsrftoken.Generate(xsrfKey, cu.login, newShortName),
488+
ReadOnly: *readonly,
486489
})
487490
}
488491

@@ -798,6 +801,10 @@ func userExists(ctx context.Context, login string) (bool, error) {
798801
var reShortName = regexp.MustCompile(`^\w[\w\-\.]*$`)
799802

800803
func serveDelete(w http.ResponseWriter, r *http.Request) {
804+
if *readonly {
805+
http.Error(w, "golink is in read-only mode", http.StatusMethodNotAllowed)
806+
return
807+
}
801808
short := strings.TrimPrefix(r.URL.Path, "/.delete/")
802809
if short == "" {
803810
http.Error(w, "short required", http.StatusBadRequest)
@@ -848,6 +855,10 @@ func serveDelete(w http.ResponseWriter, r *http.Request) {
848855
// long URL are validated for proper format. Existing links may only be updated
849856
// by their owner.
850857
func serveSave(w http.ResponseWriter, r *http.Request) {
858+
if *readonly {
859+
http.Error(w, "golink is in read-only mode", http.StatusMethodNotAllowed)
860+
return
861+
}
851862
short, long := r.FormValue("short"), r.FormValue("long")
852863
if short == "" || long == "" {
853864
http.Error(w, "short and long required", http.StatusBadRequest)
@@ -934,6 +945,9 @@ func serveSave(w http.ResponseWriter, r *http.Request) {
934945
// Admin users can edit all links.
935946
// Non-admin users can only edit their own links or links without an active owner.
936947
func canEditLink(ctx context.Context, link *Link, u user) bool {
948+
if *readonly {
949+
return false
950+
}
937951
if link == nil || link.Owner == "" {
938952
// new or unowned link
939953
return true

golink_test.go

+42
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"time"
1414

1515
"golang.org/x/net/xsrftoken"
16+
"tailscale.com/types/ptr"
1617
"tailscale.com/util/must"
1718
)
1819

@@ -357,6 +358,47 @@ func TestServeDelete(t *testing.T) {
357358
}
358359
}
359360

361+
func TestReadOnlyMode(t *testing.T) {
362+
var err error
363+
db, err = NewSQLiteDB(":memory:")
364+
if err != nil {
365+
t.Fatal(err)
366+
}
367+
db.Save(&Link{Short: "who", Long: "http://who/"})
368+
369+
oldReadOnly := readonly
370+
readonly = ptr.To(true)
371+
defer func() { readonly = oldReadOnly }()
372+
373+
// resolving link should succeed
374+
r := httptest.NewRequest("GET", "/who", nil)
375+
w := httptest.NewRecorder()
376+
serveHandler().ServeHTTP(w, r)
377+
if want := http.StatusFound; w.Code != want {
378+
t.Errorf("serveHandler() = %d; want %d", w.Code, want)
379+
}
380+
wantLocation := "http://who/"
381+
if location := w.Header().Get("Location"); location != wantLocation {
382+
t.Errorf("serveHandler() location = %v; want %v", location, wantLocation)
383+
}
384+
385+
// updating link should fail
386+
r = httptest.NewRequest("POST", "/", nil)
387+
w = httptest.NewRecorder()
388+
serveHandler().ServeHTTP(w, r)
389+
if want := http.StatusMethodNotAllowed; w.Code != want {
390+
t.Errorf("serveHandler() = %d; want %d", w.Code, want)
391+
}
392+
393+
// deleting link should fail
394+
r = httptest.NewRequest("POST", "/.delete/who", nil)
395+
w = httptest.NewRecorder()
396+
serveHandler().ServeHTTP(w, r)
397+
if want := http.StatusMethodNotAllowed; w.Code != want {
398+
t.Errorf("serveHandler() = %d; want %d", w.Code, want)
399+
}
400+
}
401+
360402
func TestExpandLink(t *testing.T) {
361403
tests := []struct {
362404
name string // test name

static/base.css

+15
Original file line numberDiff line numberDiff line change
@@ -1351,6 +1351,11 @@ select {
13511351
border-color: rgb(178 45 48 / var(--tw-border-opacity));
13521352
}
13531353

1354+
.border-orange-50 {
1355+
--tw-border-opacity: 1;
1356+
border-color: rgb(254 227 192 / var(--tw-border-opacity));
1357+
}
1358+
13541359
.bg-gray-100 {
13551360
--tw-bg-opacity: 1;
13561361
background-color: rgb(247 245 244 / var(--tw-bg-opacity));
@@ -1366,6 +1371,11 @@ select {
13661371
background-color: rgb(178 45 48 / var(--tw-bg-opacity));
13671372
}
13681373

1374+
.bg-orange-0 {
1375+
--tw-bg-opacity: 1;
1376+
background-color: rgb(255 250 238 / var(--tw-bg-opacity));
1377+
}
1378+
13691379
.p-2 {
13701380
padding: 0.5rem;
13711381
}
@@ -1390,6 +1400,11 @@ select {
13901400
padding-bottom: 1rem;
13911401
}
13921402

1403+
.py-3 {
1404+
padding-top: 0.75rem;
1405+
padding-bottom: 0.75rem;
1406+
}
1407+
13931408
.pt-6 {
13941409
padding-top: 1.5rem;
13951410
}

tailwind.config.js

+21-11
Original file line numberDiff line numberDiff line change
@@ -41,26 +41,36 @@ module.exports = {
4141
800: "rgba(90, 0, 0)",
4242
900: "rgba(66, 0, 0)",
4343
},
44-
white: '#fff',
45-
current: 'currentColor',
44+
orange: {
45+
0: "rgba(255, 250, 238)",
46+
50: "rgba(254, 227, 192)",
47+
100: "rgba(248, 184, 134)",
48+
200: "rgba(245, 146, 94)",
49+
300: "rgba(229, 111, 74)",
50+
400: "rgba(196, 76, 52)",
51+
500: "rgba(158, 47, 40)",
52+
600: "rgba(126, 30, 35)",
53+
700: "rgba(93, 22, 27)",
54+
800: "rgba(66, 14, 17)",
55+
900: "rgba(66, 14, 17)",
56+
},
57+
white: "#fff",
58+
current: "currentColor",
4659
},
4760
extend: {
4861
typography: {
4962
DEFAULT: {
5063
css: {
51-
'code::before': {
52-
'content': '',
64+
"code::before": {
65+
content: "",
5366
},
54-
'code::after': {
55-
'content': '',
67+
"code::after": {
68+
content: "",
5669
},
5770
},
5871
},
5972
},
6073
},
6174
},
62-
plugins: [
63-
require('@tailwindcss/forms'),
64-
require('@tailwindcss/typography'),
65-
],
66-
}
75+
plugins: [require("@tailwindcss/forms"), require("@tailwindcss/typography")],
76+
};

tmpl/home.html

+19-15
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
{{ define "main" }}
2-
<h2 class="text-xl font-bold pb-2">Create a new link</h2>
2+
{{ if .ReadOnly }}
3+
<p class="rounded-md py-3 px-4 bg-orange-0 border border-orange-50">{{go}} is running in read-only mode. Links can be resolved, but not created or updated.</p>
4+
{{ else }}
5+
<h2 class="text-xl font-bold pb-2">Create a new link</h2>
36

4-
{{ with .Long }}
5-
<p class="">Did you mean <a class="text-blue-600 hover:underline" href="{{.}}">{{.}}</a> ? Create a {{go}} link for it now:</p>
7+
{{ with .Long }}
8+
<p class="">Did you mean <a class="text-blue-600 hover:underline" href="{{.}}">{{.}}</a> ? Create a {{go}} link for it now:</p>
9+
{{ end }}
10+
<form method="POST" action="/" class="flex flex-wrap">
11+
<input type="hidden" name="xsrf" value="{{ .XSRF }}" />
12+
<div class="flex">
13+
<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>
14+
<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."
15+
class="p-2 my-2 rounded-r-md border-gray-300 placeholder:text-gray-400">
16+
<span class="flex m-2 items-center">&rarr;</span>
17+
</div>
18+
<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">
19+
<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>
20+
</form>
21+
<p class="text-sm text-gray-500"><a class="text-blue-600 hover:underline" href="/.help">Help and advanced options</a></p>
622
{{ end }}
7-
<form method="POST" action="/" class="flex flex-wrap">
8-
<input type="hidden" name="xsrf" value="{{ .XSRF }}" />
9-
<div class="flex">
10-
<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>
11-
<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."
12-
class="p-2 my-2 rounded-r-md border-gray-300 placeholder:text-gray-400">
13-
<span class="flex m-2 items-center">&rarr;</span>
14-
</div>
15-
<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">
16-
<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>
17-
</form>
18-
<p class="text-sm text-gray-500"><a class="text-blue-600 hover:underline" href="/.help">Help and advanced options</a></p>
1923

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

0 commit comments

Comments
 (0)