From 8890eea3226b22e449bf7d627d89f3ddf68fe716 Mon Sep 17 00:00:00 2001 From: Elara Musayelyan Date: Fri, 9 Jun 2023 10:48:46 -0700 Subject: [PATCH] Initial Commit --- .gitignore | 2 + README.md | 19 ++ go.mod | 44 +++++ go.sum | 257 ++++++++++++++++++++++++ internal/builtins/http.go | 344 +++++++++++++++++++++++++++++++++ internal/builtins/log.go | 76 ++++++++ internal/builtins/regex.go | 127 ++++++++++++ internal/builtins/register.go | 29 +++ internal/builtins/run_every.go | 84 ++++++++ internal/builtins/store.go | 98 ++++++++++ internal/builtins/updater.go | 181 +++++++++++++++++ internal/config/config.go | 27 +++ lure-updater.example.toml | 15 ++ main.go | 115 +++++++++++ 14 files changed, 1418 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/builtins/http.go create mode 100644 internal/builtins/log.go create mode 100644 internal/builtins/regex.go create mode 100644 internal/builtins/register.go create mode 100644 internal/builtins/run_every.go create mode 100644 internal/builtins/store.go create mode 100644 internal/builtins/updater.go create mode 100644 internal/config/config.go create mode 100644 lure-updater.example.toml create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3de0e4a --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/lure-updater.toml +/lure-updater \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c6f50ee --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# LURE Updater + +Modular bot that automatically checks for upstream updates and pushes new packages to [lure-repo](https://github.com/Elara6331/lure-repo). + +--- + +### How it works + +Since LURE is meant to be able to install many different types of packages, this bot accepts plugins in the form of [Starlark](https://github.com/bazelbuild/starlark) files. These plugins can schedule functions to be run at certain intervals, or when a webhook is received, and they have access to persistent key/value storage to keep track of information. This allows plugins to use many different ways to detect upstream updates. + +For example, the plugin for `discord-bin` repeatedly polls discord's API every hour for the current latest download link. It puts the link in persistent storage, and if it has changed since last time, it parses the URL to extract the version number, and uses that to update the build script for `discord-bin`. + +Another example is the plugin for `lure-bin`, which accepts a webhook from Gitea. When it receives the webhook, it parses the JSON body and gets the download URL, which it uses to download the checksum file, and uses the information inside that to update the build script for `lure-bin`. + +--- + +### Configuration + +There's an example config file in the `lure-updater.example.toml` file. Edit that to fit your needs and put it at `/etc/lure-updater/config.toml`. You can change the location of the config file using the `--config` or `-c` flag. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..59ea948 --- /dev/null +++ b/go.mod @@ -0,0 +1,44 @@ +module go.elara.ws/lure-updater + +go 1.20 + +require ( + github.com/go-git/go-git/v5 v5.7.0 + github.com/pelletier/go-toml/v2 v2.0.8 + github.com/spf13/pflag v1.0.5 + go.elara.ws/logger v0.0.0-20230421022458-e80700db2090 + go.elara.ws/pcre v0.0.0-20230421030233-daf2d2e6973f + go.etcd.io/bbolt v1.3.7 + go.starlark.net v0.0.0-20230525235612-a134d8f9ddca + golang.org/x/crypto v0.9.0 + golang.org/x/term v0.8.0 +) + +require ( + github.com/Microsoft/go-winio v0.5.2 // indirect + github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 // indirect + github.com/acomagu/bufpipe v1.0.4 // indirect + github.com/cloudflare/circl v1.3.3 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.4.1 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/gookit/color v1.5.1 // indirect + github.com/imdario/mergo v0.3.15 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect + github.com/sergi/go-diff v1.1.0 // indirect + github.com/skeema/knownhosts v1.1.1 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.8.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect + modernc.org/libc v1.16.8 // indirect + modernc.org/mathutil v1.4.1 // indirect + modernc.org/memory v1.1.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b774976 --- /dev/null +++ b/go.sum @@ -0,0 +1,257 @@ +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/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 h1:ZK3C5DtzV2nVAQTx5S5jQvMeDqWtD1By5mOoyY/xJek= +github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE= +github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= +github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= +github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4= +github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8= +github.com/go-git/go-git/v5 v5.7.0 h1:t9AudWVLmqzlo+4bqdf7GY+46SUuRsx59SboFxkq2aE= +github.com/go-git/go-git/v5 v5.7.0/go.mod h1:coJHKEOk5kUClpsNlXrUvPrDxY3w3gjHvhcZd8Fodw8= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gookit/color v1.5.1 h1:Vjg2VEcdHpwq+oY63s/ksHrgJYCTo0bwWvmmYWdE9fQ= +github.com/gookit/color v1.5.1/go.mod h1:wZFzea4X8qN6vHOSP2apMb4/+w/orMznEzYsIHPaqKM= +github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= +github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= +github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +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/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.1.1 h1:MTk78x9FPgDFVFkDLTrsnnfCJl7g1C/nnKvePgrIngE= +github.com/skeema/knownhosts v1.1.1/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= +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.elara.ws/logger v0.0.0-20230421022458-e80700db2090 h1:RVC8XvWo6Yw4HUshqx4TSzuBDScDghafU6QFRJ4xPZg= +go.elara.ws/logger v0.0.0-20230421022458-e80700db2090/go.mod h1:qng49owViqsW5Aey93lwBXONw20oGbJIoLVscB16mPM= +go.elara.ws/pcre v0.0.0-20230421030233-daf2d2e6973f h1:ZwR0xvBeP5BHHv63fgfuwhZIj+Si5rp79WSDUE73ZVA= +go.elara.ws/pcre v0.0.0-20230421030233-daf2d2e6973f/go.mod h1:EF48C6VnP4wBayzFGk6lXqbiLucH7EfiaYOgiiCe5k4= +go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= +go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY= +go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/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-20211112202133-69e39bad7dc2/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.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/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-20210330210617-4fbd30eecc44/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-20211007075335-d3039528d8ac/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.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= +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.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= +modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= +modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= +modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= +modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= +modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU= +modernc.org/libc v1.16.8 h1:Ux98PaOMvolgoFX/YwusFOHBnanXdGRmWgI8ciI2z4o= +modernc.org/libc v1.16.8/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= +modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8= +modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.1.1 h1:bDOL0DIDLQv7bWhP3gMvIrnoFw+Eo6F7a2QK9HPDiFU= +modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= +modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= +modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/internal/builtins/http.go b/internal/builtins/http.go new file mode 100644 index 0000000..dcd648e --- /dev/null +++ b/internal/builtins/http.go @@ -0,0 +1,344 @@ +package builtins + +import ( + "bytes" + "errors" + "fmt" + "io" + "net/http" + "strings" + + "go.elara.ws/logger/log" + "go.elara.ws/lure-updater/internal/config" + "go.starlark.net/starlark" + "go.starlark.net/starlarkstruct" + "golang.org/x/crypto/bcrypt" +) + +const maxBodySize = 16384 + +var ( + ErrInvalidBodyType = errors.New("invalid body type") + ErrInvalidHdrKeyType = errors.New("invalid header key type") + ErrInvalidHdrVal = errors.New("invalid header value type") + ErrInvalidType = errors.New("invalid type") + ErrInsecureWebhook = errors.New("secure webhook missing authorization") +) + +var httpModule = &starlarkstruct.Module{ + Name: "http", + Members: starlark.StringDict{ + "get": starlark.NewBuiltin("http.get", httpGet), + "post": starlark.NewBuiltin("http.post", httpPost), + "put": starlark.NewBuiltin("http.put", httpPut), + "head": starlark.NewBuiltin("http.head", httpHead), + }, +} + +func httpGet(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + return makeRequest("http.get", http.MethodGet, args, kwargs, thread) +} + +func httpPost(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + return makeRequest("http.post", http.MethodPost, args, kwargs, thread) +} + +func httpPut(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + return makeRequest("http.put", http.MethodPut, args, kwargs, thread) +} + +func httpHead(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + return makeRequest("http.head", http.MethodHead, args, kwargs, thread) +} + +type starlarkBodyReader struct { + io.Reader +} + +func (sbr *starlarkBodyReader) Unpack(v starlark.Value) error { + switch v := v.(type) { + case starlark.String: + sbr.Reader = strings.NewReader(string(v)) + case starlark.Bytes: + sbr.Reader = strings.NewReader(string(v)) + default: + return fmt.Errorf("%w: %s", ErrInvalidBodyType, v.Type()) + } + return nil +} + +func newBodyReader() *starlarkBodyReader { + return &starlarkBodyReader{ + Reader: bytes.NewReader(nil), + } +} + +type starlarkHeaders struct { + http.Header +} + +func (sh *starlarkHeaders) Unpack(v starlark.Value) error { + dict, ok := v.(*starlark.Dict) + if !ok { + return fmt.Errorf("%w: %s", ErrInvalidType, v.Type()) + } + + sh.Header = make(http.Header, dict.Len()) + for _, key := range dict.Keys() { + keyStr, ok := key.(starlark.String) + if !ok { + return fmt.Errorf("%w: %s", ErrInvalidHdrKeyType, key.Type()) + } + + val, _, _ := dict.Get(key) + list, ok := val.(*starlark.List) + if !ok { + return fmt.Errorf("%w: %s", ErrInvalidHdrVal, val.Type()) + } + + hdrVals := make([]string, list.Len()) + for i := 0; i < list.Len(); i++ { + hdrVal, ok := list.Index(i).(starlark.String) + if !ok { + return fmt.Errorf("%w: %s", ErrInvalidHdrVal, list.Index(i).Type()) + } + + hdrVals[i] = string(hdrVal) + } + + sh.Header[string(keyStr)] = hdrVals + } + + return nil +} + +func makeRequest(name, method string, args starlark.Tuple, kwargs []starlark.Tuple, thread *starlark.Thread) (starlark.Value, error) { + var ( + url string + redirect = true + headers = &starlarkHeaders{} + body = newBodyReader() + ) + err := starlark.UnpackArgs(name, args, kwargs, "url", &url, "redirect??", &redirect, "headers??", headers, "body??", body) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(method, url, body) + if err != nil { + return nil, err + } + req.Header = headers.Header + + client := http.DefaultClient + if !redirect { + client = &http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + } + + log.Debug("Making HTTP request").Str("url", url).Str("method", req.Method).Bool("redirect", redirect).Stringer("pos", thread.CallFrame(1).Pos).Send() + + res, err := client.Do(req) + if err != nil { + return nil, err + } + + log.Debug("Got HTTP response").Str("host", res.Request.URL.Host).Int("code", res.StatusCode).Stringer("pos", thread.CallFrame(1).Pos).Send() + + return starlarkResponse(res), nil +} + +func starlarkResponse(res *http.Response) *starlarkstruct.Struct { + return starlarkstruct.FromStringDict(starlarkstruct.Default, starlark.StringDict{ + "code": starlark.MakeInt(res.StatusCode), + "headers": starlarkStringSliceMap(res.Header), + "body": starlarkBody(res.Body), + }) +} + +func starlarkRequest(req *http.Request) *starlarkstruct.Struct { + return starlarkstruct.FromStringDict(starlarkstruct.Default, starlark.StringDict{ + "method": starlark.String(req.Method), + "remote_addr": starlark.String(req.RemoteAddr), + "headers": starlarkStringSliceMap(req.Header), + "query": starlarkStringSliceMap(req.URL.Query()), + "body": starlarkBody(req.Body), + }) +} + +func starlarkStringSliceMap(ssm map[string][]string) *starlark.Dict { + dict := starlark.NewDict(len(ssm)) + for key, vals := range ssm { + sVals := make([]starlark.Value, len(vals)) + for i, val := range vals { + sVals[i] = starlark.String(val) + } + dict.SetKey(starlark.String(key), starlark.NewList(sVals)) + } + return dict +} + +func starlarkBody(body io.ReadCloser) *starlarkstruct.Struct { + return starlarkstruct.FromStringDict(starlarkstruct.Default, starlark.StringDict{ + "string": bodyAsString(body), + "bytes": bodyAsBytes(body), + "close": bodyClose(body), + }) +} + +func bodyAsBytes(body io.ReadCloser) *starlark.Builtin { + return starlark.NewBuiltin("http.response.body.bytes", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + data, err := io.ReadAll(io.LimitReader(body, maxBodySize)) + if err != nil { + return nil, err + } + return starlark.Bytes(data), nil + }) +} + +func bodyAsString(body io.ReadCloser) *starlark.Builtin { + return starlark.NewBuiltin("http.response.body.string", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + data, err := io.ReadAll(io.LimitReader(body, maxBodySize)) + if err != nil { + return nil, err + } + return starlark.String(data), nil + }) +} + +func bodyClose(body io.ReadCloser) *starlark.Builtin { + return starlark.NewBuiltin("http.response.body.close", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + err := body.Close() + if err != nil { + return nil, err + } + return starlark.None, nil + }) +} + +func registerWebhook(mux *http.ServeMux, cfg *config.Config, pluginName string) *starlark.Builtin { + return starlark.NewBuiltin("register_webhook", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var fn *starlark.Function + secure := true + err := starlark.UnpackArgs("register_webhook", args, kwargs, "function", &fn, "secure??", &secure) + if err != nil { + return nil, err + } + + if !secure { + log.Warn("Plugin is registering an insecure webhook").Str("plugin", pluginName).Send() + } + + path := "/webhook/" + pluginName + "/" + fn.Name() + mux.HandleFunc(path, webhookHandler(pluginName, secure, cfg, thread, fn)) + log.Debug("Registered webhook").Str("path", path).Str("function", fn.Name()).Stringer("pos", thread.CallFrame(1).Pos).Send() + return starlark.None, nil + }) +} + +func webhookHandler(pluginName string, secure bool, cfg *config.Config, thread *starlark.Thread, fn *starlark.Function) http.HandlerFunc { + return func(res http.ResponseWriter, req *http.Request) { + defer req.Body.Close() + + res.Header().Add("X-Updater-Plugin", pluginName) + + if secure { + err := verifySecure(cfg.Webhook.PasswordHash, pluginName, req) + if err != nil { + log.Error("Error verifying webhook").Err(err).Send() + res.WriteHeader(http.StatusForbidden) + _, _ = io.WriteString(res, err.Error()) + return + } + } + + log.Debug("Calling webhook function").Str("name", fn.Name()).Stringer("pos", fn.Position()).Send() + val, err := starlark.Call(thread, fn, starlark.Tuple{starlarkRequest(req)}, nil) + if err != nil { + log.Error("Error while executing webhook").Err(err).Stringer("pos", fn.Position()).Send() + res.WriteHeader(http.StatusInternalServerError) + _, _ = io.WriteString(res, err.Error()) + return + } + + switch val := val.(type) { + case starlark.NoneType: + res.WriteHeader(http.StatusOK) + case starlark.Int: + var code int + err = starlark.AsInt(val, &code) + if err == nil { + res.WriteHeader(code) + } else { + res.WriteHeader(http.StatusOK) + } + case starlark.String, starlark.Bytes: + body := newBodyReader() + err = body.Unpack(val) + if err != nil { + log.Error("Error unpacking returned body").Err(err).Send() + return + } + _, err = io.Copy(res, body) + if err != nil { + log.Error("Error writing body").Err(err).Send() + return + } + case *starlark.Dict: + code := http.StatusOK + codeVal, ok, _ := val.Get(starlark.String("code")) + if ok { + err = starlark.AsInt(codeVal, &code) + if err != nil { + log.Error("Error decoding returned status code").Err(err).Send() + return + } + res.WriteHeader(code) + } + + body := newBodyReader() + bodyVal, ok, _ := val.Get(starlark.String("body")) + if ok { + err = body.Unpack(bodyVal) + if err != nil { + log.Error("Error unpacking returned body").Err(err).Send() + return + } + _, err = io.Copy(res, body) + if err != nil { + log.Error("Error writing body").Err(err).Send() + return + } + } + } + } +} + +func verifySecure(pwdHash, pluginName string, req *http.Request) error { + var pwd []byte + if _, pwdStr, ok := req.BasicAuth(); ok { + pwdStr = strings.TrimSpace(pwdStr) + pwd = []byte(pwdStr) + } else if hdrStr := req.Header.Get("Authorization"); hdrStr != "" { + hdrStr = strings.TrimPrefix(hdrStr, "Bearer") + hdrStr = strings.TrimSpace(hdrStr) + pwd = []byte(hdrStr) + } else { + log.Warn("Insecure webhook request"). + Str("from", req.RemoteAddr). + Str("plugin", pluginName). + Send() + return ErrInsecureWebhook + } + + fmt.Println(string(pwd)) + + if err := bcrypt.CompareHashAndPassword([]byte(pwdHash), pwd); err != nil { + return err + } + + return nil +} diff --git a/internal/builtins/log.go b/internal/builtins/log.go new file mode 100644 index 0000000..a99fcb2 --- /dev/null +++ b/internal/builtins/log.go @@ -0,0 +1,76 @@ +package builtins + +import ( + "strings" + + "go.elara.ws/logger" + "go.elara.ws/logger/log" + "go.starlark.net/starlark" + "go.starlark.net/starlarkstruct" +) + +func logModule(name string) *starlarkstruct.Module { + return &starlarkstruct.Module{ + Name: "log", + Members: starlark.StringDict{ + "debug": logDebug(name), + "info": logInfo(name), + "warn": logWarn(name), + "error": logError(name), + }, + } +} + +func logDebug(name string) *starlark.Builtin { + return starlark.NewBuiltin("log.debug", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + return starlark.None, doLogEvent(name, "log.debug", log.Debug, thread, args, kwargs) + }) +} + +func logInfo(name string) *starlark.Builtin { + return starlark.NewBuiltin("log.info", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + return starlark.None, doLogEvent(name, "log.info", log.Info, thread, args, kwargs) + }) +} + +func logWarn(name string) *starlark.Builtin { + return starlark.NewBuiltin("log.warn", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + return starlark.None, doLogEvent(name, "log.warn", log.Warn, thread, args, kwargs) + }) +} + +func logError(name string) *starlark.Builtin { + return starlark.NewBuiltin("log.error", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + return starlark.None, doLogEvent(name, "log.error", log.Error, thread, args, kwargs) + }) +} + +func doLogEvent(pluginName, fnName string, eventFn func(msg string) logger.LogBuilder, thread *starlark.Thread, args starlark.Tuple, kwargs []starlark.Tuple) error { + var msg string + var fields *starlark.Dict + err := starlark.UnpackArgs(fnName, args, kwargs, "msg", &msg, "fields??", &fields) + if err != nil { + return err + } + + evt := eventFn("[" + pluginName + "] " + msg) + + if fields != nil { + for _, key := range fields.Keys() { + val, _, err := fields.Get(key) + if err != nil { + return err + } + keyStr := strings.Trim(key.String(), `"`) + evt = evt.Stringer(keyStr, val) + } + } + + if fnName == "log.debug" { + evt = evt.Stringer("pos", thread.CallFrame(1).Pos) + } + + evt.Send() + + return nil +} diff --git a/internal/builtins/regex.go b/internal/builtins/regex.go new file mode 100644 index 0000000..4090e9d --- /dev/null +++ b/internal/builtins/regex.go @@ -0,0 +1,127 @@ +package builtins + +import ( + "sync" + + "go.elara.ws/pcre" + "go.starlark.net/starlark" + "go.starlark.net/starlarkstruct" +) + +var ( + cacheMtx = &sync.Mutex{} + regexCache = map[string]*pcre.Regexp{} +) + +var regexModule = &starlarkstruct.Module{ + Name: "regex", + Members: starlark.StringDict{ + "compile": starlark.NewBuiltin("regex.compile", regexCompile), + "compile_glob": starlark.NewBuiltin("regex.compile_glob", regexCompileGlob), + }, +} + +func regexCompile(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var regexStr string + err := starlark.UnpackArgs("regex.compile", args, kwargs, "regex", ®exStr) + if err != nil { + return nil, err + } + + cacheMtx.Lock() + regex, ok := regexCache[regexStr] + if !ok { + regex, err = pcre.Compile(regexStr) + if err != nil { + return nil, err + } + regexCache[regexStr] = regex + } + cacheMtx.Unlock() + + return starlarkRegex(regex), nil +} + +func regexCompileGlob(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var globStr string + err := starlark.UnpackArgs("regex.compile_glob", args, kwargs, "glob", &globStr) + if err != nil { + return nil, err + } + + cacheMtx.Lock() + regex, ok := regexCache[globStr] + if !ok { + regex, err = pcre.CompileGlob(globStr) + if err != nil { + return nil, err + } + regexCache[globStr] = regex + } + cacheMtx.Unlock() + + return starlarkRegex(regex), nil +} + +func starlarkRegex(regex *pcre.Regexp) *starlarkstruct.Struct { + return starlarkstruct.FromStringDict(starlarkstruct.Default, starlark.StringDict{ + "find_all": findAll(regex), + "find_one": findOne(regex), + "matches": matches(regex), + }) +} + +func findAll(regex *pcre.Regexp) *starlark.Builtin { + return starlark.NewBuiltin("regex.regexp.find_all", func(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var in string + err := starlark.UnpackArgs("regex.compile", args, kwargs, "in", &in) + if err != nil { + return nil, err + } + + matches := regex.FindAllStringSubmatch(in, -1) + return matchesToStarlark2D(matches), nil + }) +} + +func findOne(regex *pcre.Regexp) *starlark.Builtin { + return starlark.NewBuiltin("regex.regexp.find_one", func(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var in string + err := starlark.UnpackArgs("regex.compile", args, kwargs, "in", &in) + if err != nil { + return nil, err + } + + match := regex.FindStringSubmatch(in) + return matchesToStarlark1D(match), nil + }) +} + +func matches(regex *pcre.Regexp) *starlark.Builtin { + return starlark.NewBuiltin("regex.regexp.find_one", func(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var in string + err := starlark.UnpackArgs("regex.compile", args, kwargs, "in", &in) + if err != nil { + return nil, err + } + + found := regex.MatchString(in) + return starlark.Bool(found), nil + }) +} + +func matchesToStarlark2D(matches [][]string) *starlark.List { + outer := make([]starlark.Value, len(matches)) + for i, match := range matches { + outer[i] = matchesToStarlark1D(match) + } + return starlark.NewList(outer) +} + +func matchesToStarlark1D(match []string) *starlark.List { + list := make([]starlark.Value, len(match)) + for j, val := range match { + list[j] = starlark.String(val) + } + return starlark.NewList(list) +} diff --git a/internal/builtins/register.go b/internal/builtins/register.go new file mode 100644 index 0000000..455ab69 --- /dev/null +++ b/internal/builtins/register.go @@ -0,0 +1,29 @@ +package builtins + +import ( + "net/http" + + "go.elara.ws/lure-updater/internal/config" + "go.etcd.io/bbolt" + "go.starlark.net/starlark" + "go.starlark.net/starlarkjson" +) + +type Options struct { + Name string + DB *bbolt.DB + Config *config.Config + Mux *http.ServeMux +} + +func Register(sd starlark.StringDict, opts *Options) { + sd["run_every"] = starlark.NewBuiltin("run_every", runEvery) + sd["sleep"] = starlark.NewBuiltin("sleep", sleep) + sd["http"] = httpModule + sd["regex"] = regexModule + sd["store"] = storeModule(opts.DB, opts.Name) + sd["updater"] = updaterModule(opts.Config) + sd["log"] = logModule(opts.Name) + sd["json"] = starlarkjson.Module + sd["register_webhook"] = registerWebhook(opts.Mux, opts.Config, opts.Name) +} diff --git a/internal/builtins/run_every.go b/internal/builtins/run_every.go new file mode 100644 index 0000000..c07b865 --- /dev/null +++ b/internal/builtins/run_every.go @@ -0,0 +1,84 @@ +package builtins + +import ( + "sync" + "time" + + "go.elara.ws/logger/log" + "go.starlark.net/starlark" + "go.starlark.net/starlarkstruct" +) + +var ( + tickerMtx = &sync.Mutex{} + tickerCount = 0 + tickers = map[int]*time.Ticker{} +) + +func runEvery(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var every string + var fn *starlark.Function + err := starlark.UnpackArgs("run_every", args, kwargs, "every", &every, "function", &fn) + if err != nil { + return nil, err + } + + d, err := time.ParseDuration(every) + if err != nil { + return nil, err + } + + tickerMtx.Lock() + t := time.NewTicker(d) + handle := tickerCount + tickers[handle] = t + tickerCount++ + tickerMtx.Unlock() + log.Debug("Created new ticker").Int("handle", handle).Str("duration", every).Stringer("pos", thread.CallFrame(1).Pos).Send() + + go func() { + for range t.C { + log.Debug("Calling scheduled function").Str("name", fn.Name()).Stringer("pos", fn.Position()).Send() + _, err := starlark.Call(thread, fn, nil, nil) + if err != nil { + log.Warn("Error while executing scheduled function").Str("name", fn.Name()).Stringer("pos", fn.Position()).Err(err).Send() + } + } + }() + + return newTickerHandle(handle), nil +} + +func sleep(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var duration string + err := starlark.UnpackArgs("sleep", args, kwargs, "duration", &duration) + if err != nil { + return nil, err + } + + d, err := time.ParseDuration(duration) + if err != nil { + return nil, err + } + + log.Debug("Sleeping").Str("duration", duration).Stringer("pos", thread.CallFrame(1).Pos).Send() + time.Sleep(d) + return starlark.None, nil +} + +func stopTicker(handle int) *starlark.Builtin { + return starlark.NewBuiltin("stop", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + tickerMtx.Lock() + tickers[handle].Stop() + delete(tickers, handle) + tickerMtx.Unlock() + log.Debug("Stopped ticker").Int("handle", handle).Stringer("pos", thread.CallFrame(1).Pos).Send() + return starlark.None, nil + }) +} + +func newTickerHandle(handle int) starlark.Value { + return starlarkstruct.FromStringDict(starlarkstruct.Default, starlark.StringDict{ + "stop": stopTicker(handle), + }) +} diff --git a/internal/builtins/store.go b/internal/builtins/store.go new file mode 100644 index 0000000..dea1fc2 --- /dev/null +++ b/internal/builtins/store.go @@ -0,0 +1,98 @@ +package builtins + +import ( + "go.elara.ws/logger/log" + "go.etcd.io/bbolt" + "go.starlark.net/starlark" + "go.starlark.net/starlarkstruct" +) + +func storeModule(db *bbolt.DB, bucketName string) *starlarkstruct.Module { + return &starlarkstruct.Module{ + Name: "store", + Members: starlark.StringDict{ + "set": storeSet(db, bucketName), + "get": storeGet(db, bucketName), + "delete": storeDelete(db, bucketName), + }, + } +} + +func storeSet(db *bbolt.DB, bucketName string) *starlark.Builtin { + return starlark.NewBuiltin("store.set", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var key, value string + err := starlark.UnpackArgs("store.set", args, kwargs, "key", &key, "value", &value) + if err != nil { + return nil, err + } + + err = db.Update(func(tx *bbolt.Tx) error { + bucket, err := tx.CreateBucketIfNotExists([]byte(bucketName)) + if err != nil { + return err + } + err = bucket.Put([]byte(key), []byte(value)) + if err != nil { + return err + } + return nil + }) + if err != nil { + return nil, err + } + + log.Debug("Set value").Str("bucket", bucketName).Str("key", key).Str("value", value).Stringer("pos", thread.CallFrame(1).Pos).Send() + return starlark.None, nil + }) +} + +func storeGet(db *bbolt.DB, bucketName string) *starlark.Builtin { + return starlark.NewBuiltin("store.get", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var key string + err := starlark.UnpackArgs("store.get", args, kwargs, "key", &key) + if err != nil { + return nil, err + } + + var value string + err = db.Update(func(tx *bbolt.Tx) error { + bucket, err := tx.CreateBucketIfNotExists([]byte(bucketName)) + if err != nil { + return err + } + data := bucket.Get([]byte(key)) + value = string(data) + return nil + }) + if err != nil { + return nil, err + } + + log.Debug("Retrieved value").Str("bucket", bucketName).Str("key", key).Str("value", value).Stringer("pos", thread.CallFrame(1).Pos).Send() + return starlark.String(value), nil + }) +} + +func storeDelete(db *bbolt.DB, bucketName string) *starlark.Builtin { + return starlark.NewBuiltin("store.delete", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var key string + err := starlark.UnpackArgs("store.delete", args, kwargs, "key", &key) + if err != nil { + return nil, err + } + + err = db.Update(func(tx *bbolt.Tx) error { + bucket, err := tx.CreateBucketIfNotExists([]byte(bucketName)) + if err != nil { + return err + } + return bucket.Delete([]byte(key)) + }) + if err != nil { + return nil, err + } + + log.Debug("Deleted value").Str("bucket", bucketName).Str("key", key).Stringer("pos", thread.CallFrame(1).Pos).Send() + return starlark.None, nil + }) +} diff --git a/internal/builtins/updater.go b/internal/builtins/updater.go new file mode 100644 index 0000000..2c447be --- /dev/null +++ b/internal/builtins/updater.go @@ -0,0 +1,181 @@ +package builtins + +import ( + "io" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/plumbing/transport/http" + "go.elara.ws/logger/log" + "go.elara.ws/lure-updater/internal/config" + "go.starlark.net/starlark" + "go.starlark.net/starlarkstruct" +) + +func updaterModule(cfg *config.Config) *starlarkstruct.Module { + return &starlarkstruct.Module{ + Name: "updater", + Members: starlark.StringDict{ + "repo_dir": starlark.String(cfg.Git.RepoDir), + "pull": updaterPull(cfg), + "push_changes": updaterPushChanges(cfg), + "get_package_file": getPackageFile(cfg), + "write_package_file": writePackageFile(cfg), + }, + } +} + +// repoMtx makes sure two starlark threads can +// never access the repo at the same time +var repoMtx = &sync.Mutex{} + +func updaterPull(cfg *config.Config) *starlark.Builtin { + return starlark.NewBuiltin("updater.pull", func(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + repoMtx.Lock() + defer repoMtx.Unlock() + + repo, err := git.PlainOpen(cfg.Git.RepoDir) + if err != nil { + return nil, err + } + + w, err := repo.Worktree() + if err != nil { + return nil, err + } + + err = w.Pull(&git.PullOptions{Progress: os.Stderr}) + if err != git.NoErrAlreadyUpToDate && err != nil { + return nil, err + } + + return starlark.None, nil + }) +} + +func updaterPushChanges(cfg *config.Config) *starlark.Builtin { + return starlark.NewBuiltin("updater.push_changes", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var msg string + err := starlark.UnpackArgs("updater.push_changes", args, kwargs, "msg", &msg) + if err != nil { + return nil, err + } + + repoMtx.Lock() + defer repoMtx.Unlock() + + repo, err := git.PlainOpen(cfg.Git.RepoDir) + if err != nil { + return nil, err + } + + w, err := repo.Worktree() + if err != nil { + return nil, err + } + + status, err := w.Status() + if err != nil { + return nil, err + } + + if status.IsClean() { + return starlark.None, nil + } + + err = w.Pull(&git.PullOptions{Progress: os.Stderr}) + if err != git.NoErrAlreadyUpToDate && err != nil { + return nil, err + } + + _, err = w.Add(".") + if err != nil { + return nil, err + } + + sig := &object.Signature{ + Name: cfg.Git.Commit.Name, + Email: cfg.Git.Commit.Email, + When: time.Now(), + } + + h, err := w.Commit(msg, &git.CommitOptions{ + Author: sig, + Committer: sig, + }) + if err != nil { + return nil, err + } + + log.Debug("Created new commit").Stringer("hash", h).Stringer("pos", thread.CallFrame(1).Pos).Send() + + err = repo.Push(&git.PushOptions{ + Progress: os.Stderr, + Auth: &http.BasicAuth{ + Username: cfg.Git.Credentials.Username, + Password: cfg.Git.Credentials.Password, + }, + }) + if err != nil { + return nil, err + } + + log.Debug("Successfully pushed to repo").Stringer("pos", thread.CallFrame(1).Pos).Send() + + return starlark.None, nil + }) +} + +func getPackageFile(cfg *config.Config) *starlark.Builtin { + return starlark.NewBuiltin("updater.get_package_file", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var pkg, filename string + err := starlark.UnpackArgs("updater.get_package_file", args, kwargs, "pkg", &pkg, "filename", &filename) + if err != nil { + return nil, err + } + + repoMtx.Lock() + defer repoMtx.Unlock() + + path := filepath.Join(cfg.Git.RepoDir, pkg, filename) + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + log.Debug("Got package file").Str("package", pkg).Str("filename", filename).Stringer("pos", thread.CallFrame(1).Pos).Send() + return starlark.String(data), nil + }) +} + +func writePackageFile(cfg *config.Config) *starlark.Builtin { + return starlark.NewBuiltin("updater.write_package_file", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var pkg, filename, content string + err := starlark.UnpackArgs("updater.write_package_file", args, kwargs, "pkg", &pkg, "filename", &filename, "content", &content) + if err != nil { + return nil, err + } + + repoMtx.Lock() + defer repoMtx.Unlock() + + path := filepath.Join(cfg.Git.RepoDir, pkg, filename) + fl, err := os.Create(path) + if err != nil { + return nil, err + } + + _, err = io.Copy(fl, strings.NewReader(content)) + if err != nil { + return nil, err + } + + log.Debug("Wrote package file").Str("package", pkg).Str("filename", filename).Stringer("pos", thread.CallFrame(1).Pos).Send() + return starlark.None, nil + }) +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..9f7d4bf --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,27 @@ +package config + +type Config struct { + Git Git `toml:"git"` + Webhook Webhook `toml:"webhook"` +} + +type Git struct { + RepoDir string `toml:"repoDir"` + RepoURL string `toml:"repoURL"` + Commit Commit `toml:"commit"` + Credentials Credentials `toml:"credentials"` +} + +type Credentials struct { + Username string + Password string +} + +type Commit struct { + Name string `toml:"name"` + Email string `toml:"email"` +} + +type Webhook struct { + PasswordHash string `toml:"pwd_hash"` +} diff --git a/lure-updater.example.toml b/lure-updater.example.toml new file mode 100644 index 0000000..8604914 --- /dev/null +++ b/lure-updater.example.toml @@ -0,0 +1,15 @@ +[git] + repoURL = "https://github.com/Elara6331/lure-repo.git" + repoDir = "/etc/lure-updater/repo" + [git.commit] + # The name and email to use in the git commit + name = "CHANGE ME" + email = "CHANGE ME" + [git.credentials] + # Username and password for git push. Use a personal access token as the password for Github. + username = "CHANGE ME" + password = "CHANGE ME" + +[webhook] + # A hash of the webhook password. Generate one using `lure-updater -g`. + pwd_hash = "CHANGE ME" \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..519729d --- /dev/null +++ b/main.go @@ -0,0 +1,115 @@ +package main + +import ( + "fmt" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/go-git/go-git/v5" + "github.com/pelletier/go-toml/v2" + "github.com/spf13/pflag" + "go.elara.ws/logger" + "go.elara.ws/logger/log" + "go.elara.ws/lure-updater/internal/builtins" + "go.elara.ws/lure-updater/internal/config" + "go.etcd.io/bbolt" + "go.starlark.net/starlark" + "golang.org/x/crypto/bcrypt" + "golang.org/x/term" +) + +func init() { + log.Logger = logger.NewPretty(os.Stderr) + log.Logger.SetLevel(logger.LogLevelDebug) +} + +func main() { + configPath := pflag.StringP("config", "c", "/etc/lure-updater/config.toml", "Path to config file") + dbPath := pflag.StringP("database", "d", "/etc/lure-updater/db", "Path to database file") + pluginDir := pflag.StringP("plugin-dir", "p", "/etc/lure-updater/plugins", "Path to plugin directory") + serverAddr := pflag.StringP("address", "a", ":8080", "Webhook server address") + genHash := pflag.BoolP("gen-hash", "g", false, "Generate a password hash for webhooks") + pflag.Parse() + + if *genHash { + fmt.Print("Password: ") + pwd, err := term.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + log.Fatal("Error reading password").Err(err).Send() + } + hash, err := bcrypt.GenerateFromPassword(pwd, bcrypt.DefaultCost) + if err != nil { + log.Fatal("Error hashing password").Err(err).Send() + } + fmt.Printf("\n%s\n", hash) + return + } + + db, err := bbolt.Open(*dbPath, 0o644, nil) + if err != nil { + log.Fatal("Error opening database").Err(err).Send() + } + + cfg := &config.Config{} + fl, err := os.Open(*configPath) + if err != nil { + log.Fatal("Error opening config file").Err(err).Send() + } + err = toml.NewDecoder(fl).Decode(cfg) + if err != nil { + log.Fatal("Error decoding config file").Err(err).Send() + } + + if _, err := os.Stat(cfg.Git.RepoDir); os.IsNotExist(err) { + err = os.MkdirAll(cfg.Git.RepoDir, 0o755) + if err != nil { + log.Fatal("Error creating repository directory").Err(err).Send() + } + + _, err := git.PlainClone(cfg.Git.RepoDir, false, &git.CloneOptions{ + URL: cfg.Git.RepoURL, + Progress: os.Stderr, + }) + if err != nil { + log.Fatal("Error cloning repository").Err(err).Send() + } + } else if err != nil { + log.Fatal("Cannot stat configured repo directory").Err(err).Send() + } + + starFiles, err := filepath.Glob(filepath.Join(*pluginDir, "*.star")) + if err != nil { + log.Fatal("Error finding plugin files").Err(err).Send() + } + + if len(starFiles) == 0 { + log.Fatal("No plugins found. At least one plugin is required.").Send() + } + + mux := http.NewServeMux() + + for _, starFile := range starFiles { + pluginName := filepath.Base(strings.TrimSuffix(starFile, ".star")) + thread := &starlark.Thread{Name: pluginName} + + predeclared := starlark.StringDict{} + builtins.Register(predeclared, &builtins.Options{ + Name: pluginName, + Config: cfg, + DB: db, + Mux: mux, + }) + + _, err = starlark.ExecFile(thread, starFile, nil, predeclared) + if err != nil { + log.Fatal("Error executing starlark file").Str("file", starFile).Err(err).Send() + } + + log.Info("Initialized plugin").Str("name", pluginName).Send() + } + + log.Info("Starting HTTP server").Str("addr", *serverAddr).Send() + http.ListenAndServe(*serverAddr, mux) +}