From 5b65764550cd1f50cfbfc0080e11b45b87f3c0f6 Mon Sep 17 00:00:00 2001 From: Alvar Date: Mon, 13 Nov 2023 11:48:19 +0100 Subject: [PATCH 01/11] Logging: switch to log/slog As the latest Go version, 1.21, has its own structured logging framework, log/slog, I wanted to give it a try and have replaced logrus. Honestly speaking, I am not quite sure if I like log/slog, thus, maybe someday it will be exchanged again. However, I like the idea of minimizing the amount of external libraries to depend on. By this approach, the forked off child processes are now logging in JSON, which gets parsed into new logging messages from the monitor. Thus, the logging now looks more streamlined. Furthermore, I tried to unify some error messages. --- .github/workflows/ci.yml | 4 +-- CHANGELOG.md | 2 ++ go.mod | 5 +-- go.sum | 34 ------------------- gosh.go | 66 +++++++++++++++++++++++++------------ gosh_store.go | 29 +++++++++------- gosh_webserver.go | 39 +++++++++++++--------- store.go | 71 ++++++++++++++++++++++++++-------------- store_test.go | 8 +++-- subprocs.go | 49 +++++++++++++++++++++++---- webserver.go | 57 ++++++++++++++++---------------- 11 files changed, 215 insertions(+), 149 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf9f5b9..831b9dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: - go: [ '1.19', '1.20', '1.21' ] + go: [ '1.21' ] steps: - uses: actions/checkout@v4 @@ -36,7 +36,7 @@ jobs: strategy: matrix: - go: [ '1.19', '1.20', '1.21' ] + go: [ '1.21' ] goos: [ 'freebsd', 'linux', 'openbsd' ] env: diff --git a/CHANGELOG.md b/CHANGELOG.md index 4112d8f..314740d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,8 @@ Types of changes: - Made `gosh` a `chroot`ed, privilege dropped, `fork`+`exec`ed daemon. - OpenBSD installation changed due to structural program changes. - Extract web template into a more editable file, [@riotbib](https://github.com/riotbib) in [#45](https://github.com/oxzi/gosh/pull/45). +- Bumped required Go version from 1.19 to 1.21. +- Replaced logrus logging with Go's new `log/slog` and do wrapping for child processes. ### Deprecated ### Removed diff --git a/go.mod b/go.mod index 1bfc50c..e97061c 100644 --- a/go.mod +++ b/go.mod @@ -1,20 +1,17 @@ module github.com/oxzi/gosh -go 1.19 +go 1.21 require ( github.com/akamensky/base58 v0.0.0-20210829145138-ce8bf8802e8f github.com/oxzi/syscallset-go v0.1.5 - github.com/sirupsen/logrus v1.9.3 github.com/timshannon/badgerhold/v4 v4.0.3 golang.org/x/sys v0.14.0 gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/dgraph-io/badger/v3 v3.2103.5 // indirect github.com/dgraph-io/badger/v4 v4.1.0 // indirect github.com/dgraph-io/ristretto v0.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect diff --git a/go.sum b/go.sum index a2b6954..f44bd0a 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,9 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/akamensky/base58 v0.0.0-20210829145138-ce8bf8802e8f h1:z8MkSJCUyTmW5YQlxsMLBlwA7GmjxC7L4ooicxqnhz8= github.com/akamensky/base58 v0.0.0-20210829145138-ce8bf8802e8f/go.mod h1:UdUwYgAXBiL+kLfcqxoQJYkHA/vl937/PbFhZM34aZs= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= @@ -21,12 +17,8 @@ github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwc github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgraph-io/badger/v3 v3.2103.1/go.mod h1:dULbq6ehJ5K0cGW/1TQ9iSfUk0gbSiToDWmWmTsJ53E= -github.com/dgraph-io/badger/v3 v3.2103.5 h1:ylPa6qzbjYRQMU6jokoj4wzcaweHylt//CH0AKt0akg= -github.com/dgraph-io/badger/v3 v3.2103.5/go.mod h1:4MPiseMeDQ3FNCYwRbbcBOGJLf5jsE0PPFzRiKjtcdw= github.com/dgraph-io/badger/v4 v4.1.0 h1:E38jc0f+RATYrycSUf9LMv/t47XAy+3CApyYSq4APOQ= github.com/dgraph-io/badger/v4 v4.1.0/go.mod h1:P50u28d39ibBRmIJuQC/NSdBOg46HnHw7al2SW5QRHg= -github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= @@ -44,7 +36,6 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v0.0.0-20210429001901-424d2337a529/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.1.1/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= @@ -64,15 +55,12 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/flatbuffers v1.12.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v23.5.9+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg= github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= @@ -91,7 +79,6 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= -github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/compress v1.17.1 h1:NE3C767s2ak2bweCZo3+rdP4U/HoyVXLv/X9f2gPS5g= github.com/klauspost/compress v1.17.1/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= @@ -112,11 +99,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= -github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= @@ -128,16 +110,10 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/timshannon/badgerhold/v3 v3.0.0-20210909134927-2b6764d68c1e h1:zWSVsQaifg0cVH9VvR+cMguV7exK6U+SoW8YD1cZpR4= -github.com/timshannon/badgerhold/v3 v3.0.0-20210909134927-2b6764d68c1e/go.mod h1:/Seq5xGNo8jLhSbDX3jdbeZrp4yFIpQ6/7n4TjziEWs= -github.com/timshannon/badgerhold/v4 v4.0.2 h1:83OLY/NFnEaMnHEPd84bYtkLipVkjTsMbzQRYbk47g4= -github.com/timshannon/badgerhold/v4 v4.0.2/go.mod h1:rh6RyXLQFsvrvcKondPQQFZnNovpRzu+gS0FlLxYuHY= github.com/timshannon/badgerhold/v4 v4.0.3 h1:W6pd2qckoXw2cl8eH0ZCV/9CXNaXvaM26tzFi5Tj+v8= github.com/timshannon/badgerhold/v4 v4.0.3/go.mod h1:IkZIr0kcZLMdD7YJfW/G6epb6ZXHD/h0XR2BTk/VZg8= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= @@ -146,7 +122,6 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -172,7 +147,6 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= @@ -195,18 +169,12 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -215,7 +183,6 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= @@ -257,7 +224,6 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= diff --git a/gosh.go b/gosh.go index 68a5934..f30e36d 100644 --- a/gosh.go +++ b/gosh.go @@ -2,14 +2,13 @@ package main import ( "flag" + "log/slog" "os" "os/signal" "time" "golang.org/x/sys/unix" "gopkg.in/yaml.v3" - - log "github.com/sirupsen/logrus" ) // Config is the struct representation of gosh's YAML configuration file. @@ -71,30 +70,36 @@ func loadConfig(path string) (Config, error) { func mainMonitor(conf Config) { storeRpcServer, storeRpcClient, err := socketpair() if err != nil { - log.Fatal(err) + slog.Error("Failed to create socketpair", slog.Any("error", err)) + os.Exit(1) } storeFdServer, storeFdClient, err := socketpair() if err != nil { - log.Fatal(err) + slog.Error("Failed to create socketpair", slog.Any("error", err)) + os.Exit(1) } procStore, err := forkChild("store", []*os.File{storeRpcServer, storeFdServer}) if err != nil { - log.Fatal(err) + slog.Error("Failed to fork off child", slog.Any("error", err), slog.String("child", "store")) + os.Exit(1) } procWebserver, err := forkChild("webserver", []*os.File{storeRpcClient, storeFdClient}) if err != nil { - log.Fatal(err) + slog.Error("Failed to fork off child", slog.Any("error", err), slog.String("child", "webserver")) + os.Exit(1) } bottomlessPit, err := os.MkdirTemp("", "gosh-monitor-chroot") if err != nil { - log.WithError(err).Fatal("Cannot create bottomless pit jail") + slog.Error("Failed to create bottomless pit jail", slog.Any("error", err)) + os.Exit(1) } err = posixPermDrop(bottomlessPit, conf.User, conf.Group) if err != nil { - log.WithError(err).Fatal("Cannot drop permissions") + slog.Error("Failed to drop permissions", slog.Any("error", err)) + os.Exit(1) } err = restrict(restrict_linux_seccomp, @@ -104,7 +109,6 @@ func mainMonitor(conf Config) { "~@clock", "~@cpu-emulation", "~@debug", - "~@file-system", "~@keyring", "~@memlock", "~@module", @@ -118,12 +122,14 @@ func mainMonitor(conf Config) { /* @process */ "~execve", "~execveat", "~fork", }) if err != nil { - log.Fatal(err) + slog.Error("Failed to apply seccomp-bpf filter", slog.Any("error", err)) + os.Exit(1) } err = restrict(restrict_openbsd_pledge, "stdio tty proc error", "") if err != nil { - log.Fatal(err) + slog.Error("Failed to pledge", slog.Any("error", err)) + os.Exit(1) } sigintCh := make(chan os.Signal, 1) @@ -140,13 +146,13 @@ func mainMonitor(conf Config) { select { case <-sigintCh: - log.Info("Main process receives SIGINT, shutting down") + slog.Info("Main process receives SIGINT, shutting down") case <-storeCh: - log.Error("The store subprocess has stopped, cleaning up") + slog.Error("The store subprocess has stopped, cleaning up") case <-webserverCh: - log.Error("The web server subprocess has stopped, cleaning up") + slog.Error("The web server subprocess has stopped, cleaning up") } for i, childProc := range childProcs { @@ -160,9 +166,27 @@ func mainMonitor(conf Config) { } } -func main() { - log.SetFormatter(&log.TextFormatter{DisableTimestamp: true}) +// configureLogger sets the default logger with an optional debug log level and +// JSON encoded output, useful for the forked off childs. +func configureLogger(debug, jsonOutput bool) { + loggerLevel := new(slog.LevelVar) + if debug { + loggerLevel.Set(slog.LevelDebug) + } + + handlerOpts := &slog.HandlerOptions{Level: loggerLevel} + + var logger *slog.Logger + if jsonOutput { + logger = slog.New(slog.NewJSONHandler(os.Stderr, handlerOpts)) + } else { + logger = slog.New(slog.NewTextHandler(os.Stdout, handlerOpts)) + } + + slog.SetDefault(logger) +} +func main() { var ( flagConfig string flagForkChild string @@ -175,13 +199,12 @@ func main() { flag.Parse() - if flagVerbose { - log.SetLevel(log.DebugLevel) - } + configureLogger(flagVerbose, flagForkChild != "") conf, err := loadConfig(flagConfig) if err != nil { - log.WithError(err).Fatal("Cannot parse YAML configuration") + slog.Error("Failed to parse YAML configuration", slog.Any("error", err)) + os.Exit(1) } switch flagForkChild { @@ -195,6 +218,7 @@ func main() { mainMonitor(conf) default: - log.WithField("fork-child", flagForkChild).Fatal("Unknown child process") + slog.Error("Unknown child process identifier", slog.String("name", flagForkChild)) + os.Exit(1) } } diff --git a/gosh_store.go b/gosh_store.go index 5b01b71..e550314 100644 --- a/gosh_store.go +++ b/gosh_store.go @@ -1,11 +1,10 @@ package main import ( + "log/slog" "os" "os/signal" - log "github.com/sirupsen/logrus" - "golang.org/x/sys/unix" ) @@ -38,16 +37,18 @@ func ensureStoreDir(path, username, groupname string) error { } func mainStore(conf Config) { - log.WithField("config", conf.Store).Debug("Starting store child") + slog.Debug("Starting store child", slog.Any("config", conf.Store)) err := ensureStoreDir(conf.Store.Path, conf.User, conf.Group) if err != nil { - log.WithError(err).Fatal("Cannot prepare store directory") + slog.Error("Failed to prepare store directory", slog.Any("error", err)) + os.Exit(1) } err = posixPermDrop(conf.Store.Path, conf.User, conf.Group) if err != nil { - log.WithError(err).Fatal("Cannot drop permissions") + slog.Error("Failed to drop permissions", slog.Any("error", err)) + os.Exit(1) } err = restrict(restrict_linux_seccomp, @@ -70,28 +71,33 @@ func mainStore(conf Config) { /* @network-io */ "~bind", "~connect", "~listen", }) if err != nil { - log.Fatal(err) + slog.Error("Failed to apply seccomp-bpf filter", slog.Any("error", err)) + os.Exit(1) } err = restrict(restrict_openbsd_pledge, "stdio rpath wpath cpath flock unix sendfd recvfd error", "") if err != nil { - log.Fatal(err) + slog.Error("Failed to pledge", slog.Any("error", err)) + os.Exit(1) } store, err := NewStore("/", true) if err != nil { - log.Fatal(err) + slog.Error("Failed to create store", slog.Any("error", err)) + os.Exit(1) } rpcConn, err := unixConnFromFile(os.NewFile(3, "")) if err != nil { - log.Fatal(err) + slog.Error("Failed to create Unix Domain Socket from FD", slog.Any("error", err)) + os.Exit(1) } fdConn, err := unixConnFromFile(os.NewFile(4, "")) if err != nil { - log.Fatal(err) + slog.Error("Failed to create Unix Domain Socket from FD", slog.Any("error", err)) + os.Exit(1) } rpcStore := NewStoreRpcServer(store, rpcConn, fdConn) @@ -102,6 +108,7 @@ func mainStore(conf Config) { err = rpcStore.Close() if err != nil { - log.Fatal(err) + slog.Error("Failed to close RPC Store", slog.Any("error", err)) + os.Exit(1) } } diff --git a/gosh_webserver.go b/gosh_webserver.go index cddbf9c..c3b570d 100644 --- a/gosh_webserver.go +++ b/gosh_webserver.go @@ -3,14 +3,13 @@ package main import ( "fmt" "io/fs" + "log/slog" "net" "net/http" "os" "os/signal" "strconv" - log "github.com/sirupsen/logrus" - "golang.org/x/sys/unix" ) @@ -81,22 +80,25 @@ func mkListenSocket(protocol, bound, unixChmod, unixOwner, unixGroup string) (*o } func mainWebserver(conf Config) { - log.WithField("config", conf.Webserver).Debug("Starting web server child") + slog.Debug("Starting webserver child", slog.Any("config", conf.Webserver)) rpcConn, err := unixConnFromFile(os.NewFile(3, "")) if err != nil { - log.Fatal(err) + slog.Error("Failed to prepare store directory", slog.Any("error", err)) + os.Exit(1) } fdConn, err := unixConnFromFile(os.NewFile(4, "")) if err != nil { - log.Fatal(err) + slog.Error("Failed to prepare store directory", slog.Any("error", err)) + os.Exit(1) } storeClient := NewStoreRpcClient(rpcConn, fdConn) maxFilesize, err := ParseBytesize(conf.Webserver.ItemConfig.MaxSize) if err != nil { - log.WithError(err).Fatal("Failed to parse byte size") + slog.Error("Failed to parse byte size", slog.Any("error", err)) + os.Exit(1) } mimeMap := make(MimeMap) @@ -111,16 +113,19 @@ func mainWebserver(conf Config) { conf.Webserver.Listen.Protocol, conf.Webserver.Listen.Bound, conf.Webserver.UnixSocket.Chmod, conf.Webserver.UnixSocket.Owner, conf.Webserver.UnixSocket.Group) if err != nil { - log.WithError(err).Fatal("Cannot create socket to be bound to") + slog.Error("Failed to create listening socket", slog.Any("error", err)) + os.Exit(1) } bottomlessPit, err := os.MkdirTemp("", "gosh-webserver-chroot") if err != nil { - log.WithError(err).Fatal("Cannot create bottomless pit jail") + slog.Error("Failed to create bottomless pit jail", slog.Any("error", err)) + os.Exit(1) } err = posixPermDrop(bottomlessPit, conf.User, conf.Group) if err != nil { - log.WithError(err).Fatal("Cannot drop permissions") + slog.Error("Failed to drop permissions", slog.Any("error", err)) + os.Exit(1) } err = restrict(restrict_linux_seccomp, @@ -143,14 +148,16 @@ func mainWebserver(conf Config) { /* @network-io */ "~bind", "~connect", "~listen", }) if err != nil { - log.Fatal(err) + slog.Error("Failed to apply seccomp-bpf filter", slog.Any("error", err)) + os.Exit(1) } err = restrict(restrict_openbsd_pledge, "stdio unix sendfd recvfd error", "") if err != nil { - log.Fatal(err) + slog.Error("Failed to pledge", slog.Any("error", err)) + os.Exit(1) } server, err := NewServer( @@ -160,7 +167,8 @@ func mainWebserver(conf Config) { mimeMap, conf.Webserver.UrlPrefix) if err != nil { - log.WithError(err).Fatal("Cannot create web server") + slog.Error("Failed to create webserver", slog.Any("error", err)) + os.Exit(1) } defer server.Close() @@ -180,7 +188,8 @@ func mainWebserver(conf Config) { err = fmt.Errorf("unsupported protocol %q", conf.Webserver.Protocol) } if err != nil && err != http.ErrServerClosed { - log.WithError(err).Error("Web server failed to listen") + slog.Error("Webserver failed to listen", slog.Any("error", err)) + os.Exit(1) } close(serverCh) @@ -188,9 +197,9 @@ func mainWebserver(conf Config) { select { case <-sigintCh: - log.Info("Stopping web server") + slog.Info("Stopping webserver") case <-serverCh: - log.Error("Web server finished, shutting down") + slog.Error("Webserver finished, shutting down") } } diff --git a/store.go b/store.go index d195214..2080c42 100644 --- a/store.go +++ b/store.go @@ -3,13 +3,13 @@ package main import ( "crypto/rand" "errors" + "fmt" "io" + "log/slog" "os" "path/filepath" "time" - log "github.com/sirupsen/logrus" - "github.com/akamensky/base58" "github.com/timshannon/badgerhold/v4" ) @@ -23,6 +23,27 @@ const ( // the requested ID. var ErrNotFound = errors.New("No Item found for this ID") +// BadgerLogWapper implements badger.Logger to forward logs to log/slog. +type BadgerLogWapper struct { + *slog.Logger +} + +func (logger *BadgerLogWapper) Errorf(f string, args ...interface{}) { + logger.Logger.Error(fmt.Sprintf(f, args...), slog.String("producer", "badger")) +} + +func (logger *BadgerLogWapper) Warningf(f string, args ...interface{}) { + logger.Logger.Warn(fmt.Sprintf(f, args...), slog.String("producer", "badger")) +} + +func (logger *BadgerLogWapper) Infof(f string, args ...interface{}) { + logger.Logger.Info(fmt.Sprintf(f, args...), slog.String("producer", "badger")) +} + +func (logger *BadgerLogWapper) Debugf(f string, args ...interface{}) { + logger.Logger.Debug(fmt.Sprintf(f, args...), slog.String("producer", "badger")) +} + // Store stores an index of all Items as well as the pure files. type Store struct { baseDir string @@ -44,7 +65,7 @@ func NewStore(baseDir string, autoCleanup bool) (s *Store, err error) { cleanup: autoCleanup, } - log.WithField("directory", baseDir).Info("Opening Store") + slog.Info("Opening Store", slog.String("directory", baseDir)) for _, dir := range []string{baseDir, s.databaseDir(), s.storageDir()} { _, stat := os.Stat(dir) @@ -54,7 +75,7 @@ func NewStore(baseDir string, autoCleanup bool) (s *Store, err error) { err = os.Mkdir(dir, 0700) if err != nil { - log.WithField("directory", dir).WithError(err).Error("Cannot create directory") + slog.Error("Cannot create directory", slog.String("directory", dir), slog.Any("error", err)) return } } @@ -62,7 +83,7 @@ func NewStore(baseDir string, autoCleanup bool) (s *Store, err error) { opts := badgerhold.DefaultOptions opts.Dir = s.databaseDir() opts.ValueDir = opts.Dir - opts.Logger = log.StandardLogger() + opts.Logger = &BadgerLogWapper{slog.Default()} opts.Options.BaseLevelSize = 1 << 21 // 2MiB opts.Options.ValueLogFileSize = 1 << 24 // 16MiB opts.Options.BaseTableSize = 1 << 20 // 1MiB @@ -105,7 +126,7 @@ func (s *Store) cleanupExired() { case <-ticker.C: if err := s.deleteExpired(); err != nil { - log.WithError(err).Error("Deletion of expired Items failed") + slog.Error("Deletion of expired Items failed", slog.Any("error", err)) } } } @@ -147,7 +168,7 @@ func (s *Store) createID() (id string, err error) { // Close the Store and its database. func (s *Store) Close() error { - log.Info("Closing Store") + slog.Info("Closing Store") if s.cleanup { close(s.stopSyn) @@ -159,27 +180,25 @@ func (s *Store) Close() error { // Get an Item by its ID. The Item's file can be accessed with GetFile. func (s *Store) Get(id string) (i Item, err error) { - log.WithField("ID", id).Debug("Requested Item from Store") + slog.Debug("Requested Item from Store", slog.String("id", id)) err = s.bh.Get(id, &i) if err == badgerhold.ErrNotFound { - log.WithField("ID", id).Debug("Requested Item was not found") + slog.Debug("Requested Item was not found", slog.String("id", id)) err = ErrNotFound return } else if err != nil { - log.WithField("ID", id).WithError(err).Error("Requested Item failed") + slog.Error("Requesting Item failed", slog.String("id", id)) return } if s.cleanup && i.Expires.Before(time.Now()) { - log.WithFields(log.Fields{ - "ID": id, - "expires": i.Expires, - }).Info("Requested Item is expired, will be deleted") + slog.Info("Requested Item is expired, will be deleted", + slog.String("id", id), slog.Any("expires", i.Expires)) err = s.Delete(i.ID) if err != nil { - log.WithError(err).WithField("ID", id).Error("Deletion of expired Item failed") + slog.Error("Failed to delete expired Item", slog.String("id", id), slog.Any("error", err)) return } @@ -199,26 +218,28 @@ func (s *Store) GetFile(id string) (*os.File, error) { // Both a database entry and a file will be created. The given file will be // read into the storage and closed afterwards. func (s *Store) Put(i Item, file io.ReadCloser) (id string, err error) { - log.Debug("Requested insertion of Item into the Store") + slog.Debug("Requested insertion of Item into the Store") id, err = s.createID() if err != nil { - log.WithError(err).Error("Creation of an ID for a new Item failed") + slog.Error("Failed to create an ID for a new Item", slog.Any("error", err)) return } i.ID = id - log.WithField("ID", i.ID).Debug("Insert Item with assigned ID") + slog.Debug("Insert Item with assigned ID", slog.String("id", i.ID)) err = s.bh.Insert(i.ID, i) if err != nil { - log.WithField("ID", i.ID).WithError(err).Error("Insertion of an Item into database failed") + slog.Error("Failed to insert Item into database", + slog.String("id", i.ID), slog.Any("error", err)) return } f, err := os.Create(filepath.Join(s.storageDir(), i.ID)) if err != nil { - log.WithField("ID", i.ID).WithError(err).Error("Creation of file failed") + slog.Error("Failed to create file", + slog.String("id", i.ID), slog.Any("error", err)) return } @@ -249,7 +270,7 @@ func (s *Store) deleteExpired() error { } for _, i := range items { - log.WithField("ID", i.ID).Debug("Delete expired Item") + slog.Debug("Delete expired Item", slog.String("id", i.ID)) err := s.Delete(i.ID) if err != nil { return err @@ -261,17 +282,19 @@ func (s *Store) deleteExpired() error { // Delte an Item. Both the database entry and the file will be removed. func (s *Store) Delete(id string) (err error) { - log.WithField("ID", id).Debug("Requested deletion of Item") + slog.Debug("Requested deletion of Item", slog.String("id", id)) err = s.bh.Delete(&id, Item{}) if err != nil { - log.WithField("ID", id).WithError(err).Error("Deletion of Item from database failed") + slog.Error("Failed to delete Item from database", + slog.String("id", id), slog.Any("error", err)) return } err = os.Remove(filepath.Join(s.storageDir(), id)) if err != nil { - log.WithField("ID", id).WithError(err).Error("Deletion of Item from storage failed") + slog.Error("Failed to delete Item's file", + slog.String("id", id), slog.Any("error", err)) return } diff --git a/store_test.go b/store_test.go index 34dff11..3e8b57f 100644 --- a/store_test.go +++ b/store_test.go @@ -3,12 +3,11 @@ package main import ( "bytes" "io" + "log/slog" "os" "reflect" "testing" "time" - - log "github.com/sirupsen/logrus" ) // dummyReadCloser wraps around a bytes.Buffer and implements a ReadCloser. @@ -29,7 +28,10 @@ func (drc dummyReadCloser) Close() error { } func TestStore(t *testing.T) { - log.SetLevel(log.DebugLevel) + loggerLevel := new(slog.LevelVar) + loggerLevel.Set(slog.LevelDebug) + logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: loggerLevel})) + slog.SetDefault(logger) item := Item{Expires: time.Now().Add(time.Minute).UTC()} itemDataRaw := []byte("hello world") diff --git a/subprocs.go b/subprocs.go index ccc7c5d..48e78a8 100644 --- a/subprocs.go +++ b/subprocs.go @@ -2,14 +2,15 @@ package main import ( "bufio" + "context" + "encoding/json" "fmt" + "log/slog" "os" "os/exec" "os/user" "strconv" - log "github.com/sirupsen/logrus" - "golang.org/x/sys/unix" ) @@ -50,7 +51,7 @@ func socketpair() (parent, child *os.File, err error) { // The child process' output will be printed to this process' output. The // extraFiles are additional file descriptors for communication. func forkChild(child string, extraFiles []*os.File) (*os.Process, error) { - logParent, logChild, err := socketpair() + logParent, logChild, err := pipe2() if err != nil { return nil, err } @@ -58,10 +59,46 @@ func forkChild(child string, extraFiles []*os.File) (*os.Process, error) { go func() { scanner := bufio.NewScanner(logParent) for scanner.Scan() { - log.WithField("subprocess", child).Print(scanner.Text()) - if err := scanner.Err(); err != nil { - log.WithField("subprocess", child).WithError(err).Error("Scanner failed") + childLogEntry := scanner.Text() + childLogRecord := make(map[string]any) + + err := json.Unmarshal([]byte(childLogEntry), &childLogRecord) + if err != nil { + slog.Warn("Unparsable child message", + slog.String("child", child), slog.String("msg", childLogEntry), + slog.Any("error", err)) + continue + } + + logger := slog.With(slog.String("child", child)) + for k, v := range childLogRecord { + switch k { + case "time", "level", "msg": + default: + logger = logger.With(slog.Any(k, v)) + } + } + + levelVal, ok := childLogRecord["level"] + if !ok { + slog.Warn("Child messages misses level", + slog.String("child", child), slog.String("msg", childLogEntry)) + continue } + + level := new(slog.Level) + err = level.UnmarshalText([]byte(levelVal.(string))) + if err != nil { + slog.Warn("Failed to parse child's log level", + slog.String("child", child), slog.String("msg", childLogEntry), + slog.Any("error", err)) + continue + } + + logger.Log(context.Background(), *level, childLogRecord["msg"].(string)) + } + if err := scanner.Err(); err != nil { + slog.Error("Scanner failed", slog.Any("error", err)) } }() diff --git a/webserver.go b/webserver.go index 75626c3..a034e77 100644 --- a/webserver.go +++ b/webserver.go @@ -5,6 +5,7 @@ import ( "fmt" "html/template" "io" + "log/slog" "net" "net/http" "net/http/fcgi" @@ -13,8 +14,6 @@ import ( "time" _ "embed" - - log "github.com/sirupsen/logrus" ) //go:embed index.html @@ -104,7 +103,7 @@ func (serv *Server) handleRoot(w http.ResponseWriter, r *http.Request) { serv.handleUpload(w, r) default: - log.WithField("method", r.Method).Debug("Called with unsupported method") + slog.Debug("Called with unsupported method", slog.String("method", r.Method)) http.Error(w, msgUnsupportedMethod, http.StatusMethodNotAllowed) } @@ -113,7 +112,7 @@ func (serv *Server) handleRoot(w http.ResponseWriter, r *http.Request) { func (serv *Server) handleIndex(w http.ResponseWriter, r *http.Request) { t, err := template.New("index").Parse(indexTpl) if err != nil { - log.WithError(err).Error("Failed to parse template") + slog.Error("Failed to parse template", slog.Any("error", err)) http.Error(w, msgGenericError, http.StatusBadRequest) return @@ -141,29 +140,29 @@ func (serv *Server) handleIndex(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) if err := t.Execute(w, data); err != nil { - log.WithError(err).Error("Failed to execute template") + slog.Error("Failed to execute template", slog.Any("error", err)) } } func (serv *Server) handleUpload(w http.ResponseWriter, r *http.Request) { item, f, err := NewItemFromRequest(r, serv.maxSize, serv.maxLifetime) if err == ErrLifetimeTooLong { - log.Info("New Item with a too long lifetime was rejected") + slog.Info("New Item with a too long lifetime was rejected") http.Error(w, msgLifetimeExceeds, http.StatusNotAcceptable) return } else if err == ErrFileTooBig { - log.Info("New Item with a too great file size was rejected") + slog.Info("New Item with a too great file size was rejected") http.Error(w, msgFileSizeExceeds, http.StatusNotAcceptable) return } else if err != nil { - log.WithError(err).Error("Failed to create new Item") + slog.Error("Failed to create new Item", slog.Any("error", err)) http.Error(w, msgGenericError, http.StatusBadRequest) return } else if serv.mimeMap.MustDrop(item.ContentType) { - log.WithField("MIME", item.ContentType).Info("Prevented upload of an illegal MIME") + slog.Info("Prevented upload of an illegal MIME", slog.String("mime", item.ContentType)) http.Error(w, msgIllegalMime, http.StatusBadRequest) return @@ -171,16 +170,14 @@ func (serv *Server) handleUpload(w http.ResponseWriter, r *http.Request) { itemId, err := serv.store.Put(item, f, context.Background()) if err != nil { - log.WithError(err).Error("Failed to store Item") + slog.Error("Failed to store Item", slog.Any("error", err)) http.Error(w, msgGenericError, http.StatusBadRequest) return } - log.WithFields(log.Fields{ - "ID": itemId, - "expires": item.Expires, - }).Info("Uploaded new Item") + slog.Info("Uploaded new Item", + slog.String("id", itemId), slog.Any("expires", item.Expires)) w.WriteHeader(http.StatusOK) @@ -239,7 +236,7 @@ func (serv *Server) handleRequestServe(w http.ResponseWriter, r *http.Request, i func (serv *Server) handleRequest(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { - log.WithField("method", r.Method).Debug("Request with unsupported method") + slog.Debug("Request with unsupported method", slog.String("method", r.Method)) http.Error(w, msgUnsupportedMethod, http.StatusMethodNotAllowed) return @@ -250,43 +247,45 @@ func (serv *Server) handleRequest(w http.ResponseWriter, r *http.Request) { item, err := serv.store.Get(reqId, context.Background()) if err == ErrNotFound { - log.WithField("ID", reqId).Debug("Requested non-existing ID") + slog.Debug("Requested non-existing ID", slog.String("id", reqId)) http.Error(w, msgNotExists, http.StatusNotFound) return } else if err != nil { - log.WithError(err).WithField("ID", reqId).Warn("Requesting failed") + slog.Warn("Failed to request", slog.String("id", reqId), slog.Any("error", err)) http.Error(w, msgGenericError, http.StatusBadRequest) return } if serv.hasClientCachedRequest(r, item) { - log.WithField("ID", reqId).Debug("Requested with conditional GET; HTTP Status Code 304") + slog.Debug("Requested with conditional GET; HTTP Status Code 304", slog.String("id", reqId)) w.WriteHeader(http.StatusNotModified) } else { err := serv.handleRequestServe(w, r, item) if err != nil { - log.WithError(err).WithField("ID", reqId).Warn("Serving the request failed") + slog.Warn("Failed to serve request", + slog.Any("error", err), slog.String("id", reqId)) http.Error(w, msgGenericError, http.StatusBadRequest) return } } - log.WithField("ID", item.ID).Info("Item was requested") + slog.Info("Item was requested", slog.String("id", item.ID)) if item.BurnAfterReading { - log.WithField("ID", item.ID).Info("Item will be burned") + slog.Info("Item will be burned", slog.String("id", item.ID)) if err := serv.store.Delete(item.ID, context.Background()); err != nil { - log.WithError(err).WithField("ID", item.ID).Error("Deletion failed") + slog.Error("Failed to delete Item", + slog.String("id", item.ID), slog.Any("error", err)) } } } func (serv *Server) handleDeletion(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { - log.WithField("method", r.Method).Debug("Request with unsupported method") + slog.Debug("Request with unsupported method", slog.String("method", r.Method)) http.Error(w, msgUnsupportedMethod, http.StatusMethodNotAllowed) return @@ -297,7 +296,7 @@ func (serv *Server) handleDeletion(w http.ResponseWriter, r *http.Request) { reqParts := strings.Split(reqId, "/") if len(reqParts) != 3 { - log.WithField("request", reqParts).Debug("Requested URL is malformed") + slog.Debug("Requested URL is malformed", slog.Any("request", reqParts)) http.Error(w, msgGenericError, http.StatusBadRequest) return @@ -307,26 +306,26 @@ func (serv *Server) handleDeletion(w http.ResponseWriter, r *http.Request) { item, err := serv.store.Get(reqId, context.Background()) if err == ErrNotFound { - log.WithField("ID", reqId).Debug("Requested non-existing ID") + slog.Debug("Requested non-existing ID", slog.String("id", reqId)) http.Error(w, msgNotExists, http.StatusNotFound) return } else if err != nil { - log.WithError(err).WithField("ID", reqId).Warn("Requesting failed") + slog.Warn("Failed to request", slog.String("id", reqId), slog.Any("error", err)) http.Error(w, msgGenericError, http.StatusBadRequest) return } if item.DeletionKey != delKey { - log.WithField("ID", reqId).Warn("Deletion was requested with invalid key") + slog.Warn("Deletion was requested with invalid key", slog.String("id", reqId)) http.Error(w, msgDeletionKeyWrong, http.StatusForbidden) return } if err := serv.store.Delete(item.ID, context.Background()); err != nil { - log.WithError(err).WithField("ID", reqId).Error("Requested deletion failed") + slog.Error("Failed to delete", slog.String("id", reqId), slog.Any("error", err)) http.Error(w, msgGenericError, http.StatusBadRequest) return @@ -335,7 +334,7 @@ func (serv *Server) handleDeletion(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) fmt.Fprintln(w, msgDeletionSuccess) - log.WithField("ID", reqId).Info("Item was deleted by request") + slog.Info("Item was deleted by request", slog.String("id", reqId)) } // WebProtocol returns "http" or "https", based either on the X-Forwarded-Proto From cb94598e4d3347d896c4d9fd3e372b640af17b61 Mon Sep 17 00:00:00 2001 From: Alvar Date: Mon, 13 Nov 2023 12:31:34 +0100 Subject: [PATCH 02/11] Drop now unused MimeMap type After switching to the YAML configuration, the old MIME map file with its MimeMap type became obsolete. This commit now cleans up and finally removed the MimeMap type. --- gosh_webserver.go | 10 +++---- mimemap.go | 75 ----------------------------------------------- mimemap_test.go | 75 ----------------------------------------------- webserver.go | 23 ++++++++++----- 4 files changed, 20 insertions(+), 163 deletions(-) delete mode 100644 mimemap.go delete mode 100644 mimemap_test.go diff --git a/gosh_webserver.go b/gosh_webserver.go index c3b570d..342253a 100644 --- a/gosh_webserver.go +++ b/gosh_webserver.go @@ -101,12 +101,9 @@ func mainWebserver(conf Config) { os.Exit(1) } - mimeMap := make(MimeMap) + mimeDrop := make(map[string]struct{}) for _, key := range conf.Webserver.ItemConfig.MimeDrop { - mimeMap[key] = MimeDrop - } - for key, value := range conf.Webserver.ItemConfig.MimeMap { - mimeMap[key] = value + mimeDrop[key] = struct{}{} } fd, err := mkListenSocket( @@ -164,7 +161,8 @@ func mainWebserver(conf Config) { storeClient, maxFilesize, conf.Webserver.ItemConfig.MaxLifetime, conf.Webserver.Contact, - mimeMap, + mimeDrop, + conf.Webserver.ItemConfig.MimeMap, conf.Webserver.UrlPrefix) if err != nil { slog.Error("Failed to create webserver", slog.Any("error", err)) diff --git a/mimemap.go b/mimemap.go deleted file mode 100644 index afbf8ce..0000000 --- a/mimemap.go +++ /dev/null @@ -1,75 +0,0 @@ -package main - -import ( - "bufio" - "errors" - "fmt" - "io" - "strings" -) - -const MimeDrop = "DROP" - -var ErrMimeDrop = errors.New("MIME must be dropped") - -// MimeMap replaces predefined MIME types with others or requires them to be dropped. -// -// # An example MimeMap could look like this, comment included: -// text/html text/plain -// text/javascript text/plain -// text/mp4 DROP -// -type MimeMap map[string]string - -// NewMimeMap creates a new MimeMap based on the Reader's data. -func NewMimeMap(file io.Reader) (mm MimeMap, err error) { - mm = make(MimeMap) - - scanner := bufio.NewScanner(file) - for i := 1; scanner.Scan(); i++ { - mmLine := scanner.Text() - if mmLine == "" || strings.HasPrefix(mmLine, "#") { - continue - } - - mmFields := strings.Fields(mmLine) - if l := len(mmFields); l != 2 { - err = fmt.Errorf("Entry in line %d has %d instead of 2 fields", i, l) - return - } - - mmKey, mmVal := mmFields[0], mmFields[1] - if _, exists := mm[mmKey]; exists { - err = fmt.Errorf("Key %q from line %d was already defined", mmKey, i) - return - } else { - mm[mmKey] = mmVal - } - } - - if scannerErr := scanner.Err(); scannerErr != nil { - err = scannerErr - return - } - - return -} - -// MustDrop indicates if a MIME type must be dropped. -func (mm MimeMap) MustDrop(mime string) bool { - v, exists := mm[mime] - return exists && (v == MimeDrop) -} - -// Substitute returns the replaced MIME type and indicates with an error, if -// the input MIME type must be dropped. -func (mm MimeMap) Substitute(mime string) (mimeOut string, err error) { - if v, exists := mm[mime]; !exists { - mimeOut = mime - } else if v == MimeDrop { - err = ErrMimeDrop - } else { - mimeOut = v - } - return -} diff --git a/mimemap_test.go b/mimemap_test.go deleted file mode 100644 index 0911d0b..0000000 --- a/mimemap_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package main - -import ( - "bytes" - "strings" - "testing" -) - -func TestNewMimeMap(t *testing.T) { - mm1 := "" - mm2 := "# ignore me" - mm3 := "text/html text/plain\ntext/javascript text/plain" - mm4 := "text/html text/plain\ntext/html text/duplicate" - mm5 := "foo" - mm6 := "foo bar buz" - - tests := []struct { - mmText string - valid bool - }{ - {mm1, true}, - {mm2, true}, - {mm3, true}, - {mm4, false}, - {mm5, false}, - {mm6, false}, - } - - for _, test := range tests { - buff := bytes.NewBufferString(test.mmText) - if _, err := NewMimeMap(buff); (err == nil) != test.valid { - t.Fatalf("%s resulted in %v", test.mmText, err) - } - } -} - -func TestMimeMap(t *testing.T) { - mmBuff := bytes.NewBufferString(strings.TrimSpace(` -# Just a comment -text/html text/plain -text/javascript text/plain - -# such empty lines, wow - -video/mp4 DROP - `)) - - mm, mmErr := NewMimeMap(mmBuff) - if mmErr != nil { - t.Fatal(mmErr) - } - - tests := []struct { - input string - output string - valid bool - drop bool - }{ - {"text/html", "text/plain", true, false}, - {"text/javascript", "text/plain", true, false}, - {"video/mp4", "", false, true}, - } - - for _, test := range tests { - if out, err := mm.Substitute(test.input); (err == nil) != test.valid { - t.Fatalf("%s resulted in %v", test.input, err) - } else if test.valid && out != test.output { - t.Fatalf("%s mapped to %s, instead of %s", test.input, out, test.output) - } - - if drop := mm.MustDrop(test.input); drop != test.drop { - t.Fatalf("%s should be dropped: %t instead of %t", test.input, drop, test.drop) - } - } -} diff --git a/webserver.go b/webserver.go index a034e77..f7a466f 100644 --- a/webserver.go +++ b/webserver.go @@ -36,19 +36,28 @@ type Server struct { maxSize int64 maxLifetime time.Duration contactMail string - mimeMap MimeMap + mimeDrop map[string]struct{} + mimeMap map[string]string urlPrefix string } // NewServer creates a new Server with a given database directory, and // configuration values. The Server must be started as an http.Handler. -func NewServer(store *StoreRpcClient, maxSize int64, maxLifetime time.Duration, - contactMail string, mimeMap MimeMap, urlPrefix string) (s *Server, err error) { +func NewServer( + store *StoreRpcClient, + maxSize int64, + maxLifetime time.Duration, + contactMail string, + mimeDrop map[string]struct{}, + mimeMap map[string]string, + urlPrefix string, +) (s *Server, err error) { s = &Server{ store: store, maxSize: maxSize, maxLifetime: maxLifetime, contactMail: contactMail, + mimeDrop: mimeDrop, mimeMap: mimeMap, urlPrefix: urlPrefix, } @@ -161,7 +170,7 @@ func (serv *Server) handleUpload(w http.ResponseWriter, r *http.Request) { http.Error(w, msgGenericError, http.StatusBadRequest) return - } else if serv.mimeMap.MustDrop(item.ContentType) { + } else if _, drop := serv.mimeDrop[item.ContentType]; drop { slog.Info("Prevented upload of an illegal MIME", slog.String("mime", item.ContentType)) http.Error(w, msgIllegalMime, http.StatusBadRequest) @@ -214,9 +223,9 @@ func (serv *Server) handleRequestServe(w http.ResponseWriter, r *http.Request, i defer f.Close() - mimeType, err := serv.mimeMap.Substitute(item.ContentType) - if err != nil { - return fmt.Errorf("substituting MIME failed: %v", err) + mimeType := item.ContentType + if mimeSubst, ok := serv.mimeMap[mimeType]; ok { + mimeType = mimeSubst } w.Header().Set("Content-Type", mimeType) From 307b4162a692bc33569d17c86d9fb3bc40aa0b3b Mon Sep 17 00:00:00 2001 From: Alvar Date: Mon, 13 Nov 2023 14:17:06 +0100 Subject: [PATCH 03/11] Configurable index template and static files Iterating on riotbib's Pull Request #45, it is now possible to overwrite the index.html template file during startup. Furthermore, static files are now also supported, e.g., for stylesheets. --- CHANGELOG.md | 2 +- gosh.go | 12 +++++++++++ gosh.yml | 14 +++++++++++++ gosh_webserver.go | 46 +++++++++++++++++++++++++++++++++++++++-- webserver.go | 52 ++++++++++++++++++++++++++++++++++++++--------- 5 files changed, 113 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 314740d..bfd4480 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ Types of changes: - Add goshy as bash script and NixOS program, [@riotbib](https://github.com/riotbib) in [#27](https://github.com/oxzi/gosh/pull/27). - Created Store RPC working on Unix domain sockets to allow a `fork`+`exec`ed daemon. - Configuration through YAML configuration file. +- Configurable index template and static files, partially by [@riotbib](https://github.com/riotbib) in [#45](https://github.com/oxzi/gosh/pull/45). ### Changed - Dependency version bumps. @@ -30,7 +31,6 @@ Types of changes: - `goshd` became `gosh`. - Made `gosh` a `chroot`ed, privilege dropped, `fork`+`exec`ed daemon. - OpenBSD installation changed due to structural program changes. -- Extract web template into a more editable file, [@riotbib](https://github.com/riotbib) in [#45](https://github.com/oxzi/gosh/pull/45). - Bumped required Go version from 1.19 to 1.21. - Replaced logrus logging with Go's new `log/slog` and do wrapping for child processes. diff --git a/gosh.go b/gosh.go index f30e36d..4270dd3 100644 --- a/gosh.go +++ b/gosh.go @@ -11,6 +11,14 @@ import ( "gopkg.in/yaml.v3" ) +// StaticFileConfig describes a static_files from the YAML and holds its data. +type StaticFileConfig struct { + Path string `yaml:"path"` + Mime string `yaml:"mime"` + + data []byte +} + // Config is the struct representation of gosh's YAML configuration file. // // For each field's meaning, please consider the gosh.yml file in this @@ -40,6 +48,10 @@ type Config struct { UrlPrefix string `yaml:"url_prefix"` + CustomIndex string `yaml:"custom_index"` + + StaticFiles map[string]StaticFileConfig `yaml:"static_files"` + ItemConfig struct { MaxSize string `yaml:"max_size"` MaxLifetime time.Duration `yaml:"max_lifetime"` diff --git a/gosh.yml b/gosh.yml index 7d9ea51..d966d8a 100644 --- a/gosh.yml +++ b/gosh.yml @@ -39,6 +39,20 @@ webserver: # url_prefix is an optional prefix in URL to be used, e.g., "/gosh" url_prefix: "" + # custom_index will be used instead of the compiled in index.html template. + # For starters, copy the index.html from the repository somewhere nice. + custom_index: "/path/to/alternative/index.html" + + # static_files to be read during startup and returned instead of being passed + # against the store's database. This might be used for custom resources. + static_files: + "/favicon.ico": + path: "/path/to/favicon.ico" + mime: "image/vnd.microsoft.icon" + "/custom.css": + path: "/path/to/custom.css" + mime: "text/css" + # item_config sets restrictions for new items, e.g., their max_size, in bytes # or suffixed with a unit, and max_lifetime, as a Go duration. Furthermore, # some MIME types might be dropped by mime_drop or rewritten with mime_map. diff --git a/gosh_webserver.go b/gosh_webserver.go index 342253a..f93cb1a 100644 --- a/gosh_webserver.go +++ b/gosh_webserver.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "io" "io/fs" "log/slog" "net" @@ -95,6 +96,43 @@ func mainWebserver(conf Config) { storeClient := NewStoreRpcClient(rpcConn, fdConn) + indexTpl := "" + if conf.Webserver.CustomIndex != "" { + f, err := os.Open(conf.Webserver.CustomIndex) + if err != nil { + slog.Error("Failed to open custom index file", slog.Any("error", err)) + os.Exit(1) + } + + indexTplRaw, err := io.ReadAll(f) + if err != nil { + slog.Error("Failed to read custom index file", slog.Any("error", err)) + os.Exit(1) + } + _ = f.Close() + + indexTpl = string(indexTplRaw) + } + + for k, sfc := range conf.Webserver.StaticFiles { + f, err := os.Open(sfc.Path) + if err != nil { + slog.Error("Failed to open static file", + slog.String("file", sfc.Path), slog.Any("error", err)) + os.Exit(1) + } + + sfc.data, err = io.ReadAll(f) + if err != nil { + slog.Error("Failed to read static file", + slog.String("file", sfc.Path), slog.Any("error", err)) + os.Exit(1) + } + _ = f.Close() + + conf.Webserver.StaticFiles[k] = sfc + } + maxFilesize, err := ParseBytesize(conf.Webserver.ItemConfig.MaxSize) if err != nil { slog.Error("Failed to parse byte size", slog.Any("error", err)) @@ -159,11 +197,15 @@ func mainWebserver(conf Config) { server, err := NewServer( storeClient, - maxFilesize, conf.Webserver.ItemConfig.MaxLifetime, + maxFilesize, + conf.Webserver.ItemConfig.MaxLifetime, conf.Webserver.Contact, mimeDrop, conf.Webserver.ItemConfig.MimeMap, - conf.Webserver.UrlPrefix) + conf.Webserver.UrlPrefix, + indexTpl, + conf.Webserver.StaticFiles, + ) if err != nil { slog.Error("Failed to create webserver", slog.Any("error", err)) os.Exit(1) diff --git a/webserver.go b/webserver.go index f7a466f..ada1464 100644 --- a/webserver.go +++ b/webserver.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "context" "fmt" "html/template" @@ -17,7 +18,7 @@ import ( ) //go:embed index.html -var indexTpl string +var defaultIndexTpl string const ( msgDeletionKeyWrong = "Error: Deletion key is incorrect." @@ -39,6 +40,8 @@ type Server struct { mimeDrop map[string]struct{} mimeMap map[string]string urlPrefix string + indexTpl *template.Template + staticFiles map[string]StaticFileConfig } // NewServer creates a new Server with a given database directory, and @@ -51,7 +54,19 @@ func NewServer( mimeDrop map[string]struct{}, mimeMap map[string]string, urlPrefix string, + indexTplRaw string, + staticFiles map[string]StaticFileConfig, ) (s *Server, err error) { + indexTpl := defaultIndexTpl + if indexTplRaw != "" { + indexTpl = indexTplRaw + } + + t, err := template.New("index").Parse(indexTpl) + if err != nil { + return nil, err + } + s = &Server{ store: store, maxSize: maxSize, @@ -60,6 +75,8 @@ func NewServer( mimeDrop: mimeDrop, mimeMap: mimeMap, urlPrefix: urlPrefix, + indexTpl: t, + staticFiles: staticFiles, } return } @@ -98,6 +115,8 @@ func (serv *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { serv.handleRoot(w, r) } else if strings.HasPrefix(reqPath, "/del/") { serv.handleDeletion(w, r) + } else if stc, ok := serv.staticFiles[reqPath]; ok { + serv.handleStaticFile(w, r, stc) } else { serv.handleRequest(w, r) } @@ -119,14 +138,6 @@ func (serv *Server) handleRoot(w http.ResponseWriter, r *http.Request) { } func (serv *Server) handleIndex(w http.ResponseWriter, r *http.Request) { - t, err := template.New("index").Parse(indexTpl) - if err != nil { - slog.Error("Failed to parse template", slog.Any("error", err)) - - http.Error(w, msgGenericError, http.StatusBadRequest) - return - } - data := struct { Expires string Size string @@ -148,11 +159,32 @@ func (serv *Server) handleIndex(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html;charset=UTF-8") w.WriteHeader(http.StatusOK) - if err := t.Execute(w, data); err != nil { + if err := serv.indexTpl.Execute(w, data); err != nil { slog.Error("Failed to execute template", slog.Any("error", err)) } } +func (serv *Server) handleStaticFile(w http.ResponseWriter, r *http.Request, sfc StaticFileConfig) { + if r.Method != http.MethodGet { + slog.Debug("Request with unsupported method", slog.String("method", r.Method)) + + http.Error(w, msgUnsupportedMethod, http.StatusMethodNotAllowed) + return + } + + w.Header().Set("Content-Type", sfc.Mime) + w.WriteHeader(http.StatusOK) + + staticReader := bytes.NewReader(sfc.data) + _, err := io.Copy(w, staticReader) + if err != nil { + slog.Error("Failed to write static file back to request", slog.Any("error", err)) + + http.Error(w, msgGenericError, http.StatusBadRequest) + return + } +} + func (serv *Server) handleUpload(w http.ResponseWriter, r *http.Request) { item, f, err := NewItemFromRequest(r, serv.maxSize, serv.maxLifetime) if err == ErrLifetimeTooLong { From 9a5b125461c2e41bc62be8e49cc453c3d7abfb8b Mon Sep 17 00:00:00 2001 From: Alvar Date: Mon, 13 Nov 2023 20:27:19 +0100 Subject: [PATCH 04/11] Fix deprecated rand.Seed in tests --- item_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/item_test.go b/item_test.go index 27c936a..1810643 100644 --- a/item_test.go +++ b/item_test.go @@ -94,8 +94,7 @@ func TestItem(t *testing.T) { writer := multipart.NewWriter(buff) tmpFileData := make([]byte, test.size) - rand.Seed(0) - rand.Read(tmpFileData) + rand.New(rand.NewSource(0)).Read(tmpFileData) if f, err := writer.CreateFormFile(formFile, test.filename); err != nil { t.Fatal(err) From f11b50f89b9224b589f937d1d1e53f6990fb7933 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lennart=20M=C3=BChlenmeier?= Date: Sun, 12 Nov 2023 18:47:42 +0100 Subject: [PATCH 05/11] Add meta tag with viewport settings to index.html This change improves responsibility for mobile devices. --- index.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/index.html b/index.html index 9f78e4d..2ddf407 100644 --- a/index.html +++ b/index.html @@ -3,6 +3,8 @@ gosh! Go Share + +