aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore6
-rw-r--r--.reuse/dep58
-rw-r--r--LICENSES/CC0-1.0.txt119
-rw-r--r--LICENSES/MIT.txt19
-rw-r--r--README.md56
-rw-r--r--cmd/dispatch/main.go120
-rw-r--r--config/config.go141
-rw-r--r--go.mod14
-rw-r--r--go.sum18
9 files changed, 501 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2423481
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+# SPDX-FileCopyrightText: 2020 Ethel Morgan
+#
+# SPDX-License-Identifier: MIT
+
+/config.json
+/dispatch
diff --git a/.reuse/dep5 b/.reuse/dep5
new file mode 100644
index 0000000..3afb474
--- /dev/null
+++ b/.reuse/dep5
@@ -0,0 +1,8 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: Dispatch
+Upstream-Contact: Ethel Morgan <eth@ethulhu.co.uk>
+Source: https://git.eth.moe/dispatch
+
+Files: go.sum
+Copyright: n/a
+License: CC0-1.0
diff --git a/LICENSES/CC0-1.0.txt b/LICENSES/CC0-1.0.txt
new file mode 100644
index 0000000..a343ccd
--- /dev/null
+++ b/LICENSES/CC0-1.0.txt
@@ -0,0 +1,119 @@
+Creative Commons Legal Code
+
+CC0 1.0 Universal CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES
+NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE
+AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION
+ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE
+OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS
+LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION
+OR WORKS PROVIDED HEREUNDER.
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer exclusive
+Copyright and Related Rights (defined below) upon the creator and subsequent
+owner(s) (each and all, an "owner") of an original work of authorship and/or
+a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for the
+purpose of contributing to a commons of creative, cultural and scientific
+works ("Commons") that the public can reliably and without fear of later claims
+of infringement build upon, modify, incorporate in other works, reuse and
+redistribute as freely as possible in any form whatsoever and for any purposes,
+including without limitation commercial purposes. These owners may contribute
+to the Commons to promote the ideal of a free culture and the further production
+of creative, cultural and scientific works, or to gain reputation or greater
+distribution for their Work in part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any expectation
+of additional consideration or compensation, the person associating CC0 with
+a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright
+and Related Rights in the Work, voluntarily elects to apply CC0 to the Work
+and publicly distribute the Work under its terms, with knowledge of his or
+her Copyright and Related Rights in the Work and the meaning and intended
+legal effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be protected
+by copyright and related or neighboring rights ("Copyright and Related Rights").
+Copyright and Related Rights include, but are not limited to, the following:
+
+i. the right to reproduce, adapt, distribute, perform, display, communicate,
+and translate a Work;
+
+ ii. moral rights retained by the original author(s) and/or performer(s);
+
+iii. publicity and privacy rights pertaining to a person's image or likeness
+depicted in a Work;
+
+iv. rights protecting against unfair competition in regards to a Work, subject
+to the limitations in paragraph 4(a), below;
+
+v. rights protecting the extraction, dissemination, use and reuse of data
+in a Work;
+
+vi. database rights (such as those arising under Directive 96/9/EC of the
+European Parliament and of the Council of 11 March 1996 on the legal protection
+of databases, and under any national implementation thereof, including any
+amended or successor version of such directive); and
+
+vii. other similar, equivalent or corresponding rights throughout the world
+based on applicable law or treaty, and any national implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention of,
+applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
+unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
+and Related Rights and associated claims and causes of action, whether now
+known or unknown (including existing as well as future claims and causes of
+action), in the Work (i) in all territories worldwide, (ii) for the maximum
+duration provided by applicable law or treaty (including future time extensions),
+(iii) in any current or future medium and for any number of copies, and (iv)
+for any purpose whatsoever, including without limitation commercial, advertising
+or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the
+benefit of each member of the public at large and to the detriment of Affirmer's
+heirs and successors, fully intending that such Waiver shall not be subject
+to revocation, rescission, cancellation, termination, or any other legal or
+equitable action to disrupt the quiet enjoyment of the Work by the public
+as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason be
+judged legally invalid or ineffective under applicable law, then the Waiver
+shall be preserved to the maximum extent permitted taking into account Affirmer's
+express Statement of Purpose. In addition, to the extent the Waiver is so
+judged Affirmer hereby grants to each affected person a royalty-free, non
+transferable, non sublicensable, non exclusive, irrevocable and unconditional
+license to exercise Affirmer's Copyright and Related Rights in the Work (i)
+in all territories worldwide, (ii) for the maximum duration provided by applicable
+law or treaty (including future time extensions), (iii) in any current or
+future medium and for any number of copies, and (iv) for any purpose whatsoever,
+including without limitation commercial, advertising or promotional purposes
+(the "License"). The License shall be deemed effective as of the date CC0
+was applied by Affirmer to the Work. Should any part of the License for any
+reason be judged legally invalid or ineffective under applicable law, such
+partial invalidity or ineffectiveness shall not invalidate the remainder of
+the License, and in such case Affirmer hereby affirms that he or she will
+not (i) exercise any of his or her remaining Copyright and Related Rights
+in the Work or (ii) assert any associated claims and causes of action with
+respect to the Work, in either case contrary to Affirmer's express Statement
+of Purpose.
+
+ 4. Limitations and Disclaimers.
+
+a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered,
+licensed or otherwise affected by this document.
+
+b. Affirmer offers the Work as-is and makes no representations or warranties
+of any kind concerning the Work, express, implied, statutory or otherwise,
+including without limitation warranties of title, merchantability, fitness
+for a particular purpose, non infringement, or the absence of latent or other
+defects, accuracy, or the present or absence of errors, whether or not discoverable,
+all to the greatest extent permissible under applicable law.
+
+c. Affirmer disclaims responsibility for clearing rights of other persons
+that may apply to the Work or any use thereof, including without limitation
+any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims
+responsibility for obtaining any necessary consents, permissions or other
+rights required for any use of the Work.
+
+d. Affirmer understands and acknowledges that Creative Commons is not a party
+to this document and has no duty or obligation with respect to this CC0 or
+use of the Work.
diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt
new file mode 100644
index 0000000..204b93d
--- /dev/null
+++ b/LICENSES/MIT.txt
@@ -0,0 +1,19 @@
+MIT License Copyright (c) <year> <copyright holders>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice (including the next
+paragraph) shall be included in all copies or substantial portions of the
+Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
+OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..79cddfb
--- /dev/null
+++ b/README.md
@@ -0,0 +1,56 @@
+<!--
+SPDX-FileCopyrightText: 2020 Ethel Morgan
+
+SPDX-License-Identifier: CC0-1.0
+-->
+
+# Dispatch Server
+
+## Config
+
+For example,
+
+```json
+{
+ "actions": {
+ "update Catbus UI": {
+ "triggers": [
+ {
+ "url": "/gitolite-repo-updated",
+ "formValues": {
+ "repo": "catbus-web-ui"
+ }
+ }
+ ],
+ "outputs": [
+ {
+ "url": "https://build.eth.moe/deploy",
+ "formValues": {
+ "project": "catbus-web-ui"
+ }
+ }
+ ]
+ },
+ "turn lights on": {
+ "triggers": [
+ {
+ "url": "/living-room/lights-on",
+ "formValues": {
+ "power": "on"
+ }
+ }
+ ],
+ "outputs": [
+ {
+ "mqtt": "tcp://catbus.eth.moe/home/living-room/sofa-light/power",
+ "value": "on"
+ },
+ {
+ "mqtt": "tcp://catbus.eth.moe/home/living-room/front-light/power",
+ "value": "on"
+ }
+ ]
+ }
+ }
+}
+```
diff --git a/cmd/dispatch/main.go b/cmd/dispatch/main.go
new file mode 100644
index 0000000..53aec92
--- /dev/null
+++ b/cmd/dispatch/main.go
@@ -0,0 +1,120 @@
+// SPDX-FileCopyrightText: 2020 Ethel Morgan
+//
+// SPDX-License-Identifier: MIT
+
+// Binary dispatch is a webhook dispatch server.
+package main
+
+import (
+ "context"
+ "net"
+ "net/http"
+
+ "github.com/gorilla/mux"
+ "go.eth.moe/dispatch/config"
+ "go.eth.moe/flag"
+ "go.eth.moe/httputil"
+ "go.eth.moe/logger"
+)
+
+var (
+ configPath = flag.Custom("config-path", "", "path to config.json", flag.RequiredString)
+
+ listen = flag.Custom("listen", "", "either a unix socket path (e.g. /run/nginx/catbus.sock), a port prefixed by a colon (e.g. :8080), or an IP and port (e.g. 192.168.1.1:8080)", func(raw string) (interface{}, error) {
+ if raw == "" {
+ return nil, flag.ErrRequired
+ }
+ if conn, err := net.ResolveTCPAddr("tcp", raw); err == nil {
+ return conn, err
+ }
+ return net.ResolveUnixAddr("unix", raw)
+ })
+)
+
+func main() {
+ flag.Parse()
+
+ configPath := (*configPath).(string)
+ listen := (*listen).(net.Addr)
+
+ log := logger.Background()
+
+ config, err := config.ParseFile(configPath)
+ if err != nil {
+ log.AddField("config-path", configPath)
+ log.WithError(err).Fatal("could not parse config")
+ }
+
+ log.AddField("http.listen", listen)
+ conn, err := net.Listen(listen.Network(), listen.String())
+ if err != nil {
+ log.WithError(err).Fatal("could not listen")
+ }
+ defer conn.Close()
+
+ m := mux.NewRouter()
+ m.NotFoundHandler = httputil.NotFoundHandler
+
+ m.PathPrefix("/actions/{action}").
+ Methods("POST").
+ Handler(http.StripPrefix("/actions", triggerAction(config)))
+
+ m.Use(httputil.Logger)
+
+ log.Info("starting HTTP server")
+ if err := http.Serve(conn, m); err != nil {
+ log.WithError(err).Fatal("could not start HTTP server")
+ }
+}
+
+func triggerAction(config *config.Config) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ found := false
+ for _, actionConfig := range config.Actions {
+ if matchAction(actionConfig, r) {
+ found = true
+ runAction(ctx, actionConfig)
+ }
+ }
+ if !found {
+ httputil.NotFound(w, r)
+ return
+ }
+ }
+}
+
+func matchAction(action config.Action, r *http.Request) bool {
+ for _, trigger := range action.Triggers {
+ if trigger.URL.Path != r.URL.Path {
+ return false
+ }
+ for k := range trigger.FormValues {
+ want := trigger.FormValues.Get(k)
+ got := r.FormValue(k)
+ if got != want {
+ return false
+ }
+ }
+ return true
+ }
+ return false
+}
+
+func runAction(ctx context.Context, action config.Action) {
+ log, _ := logger.FromContext(ctx)
+
+ for _, output := range action.Outputs {
+ if output.Kind != config.HTTPOutput {
+ log.Warning("only supports HTTP for now")
+ continue
+ }
+
+ if _, err := http.PostForm(output.URL.String(), output.FormValues); err != nil {
+ log.WithError(err).Error("could not POST form")
+ continue
+ }
+ log.Info("POSTed to URL")
+ }
+}
diff --git a/config/config.go b/config/config.go
new file mode 100644
index 0000000..38bf73c
--- /dev/null
+++ b/config/config.go
@@ -0,0 +1,141 @@
+// SPDX-FileCopyrightText: 2020 Ethel Morgan
+//
+// SPDX-License-Identifier: MIT
+
+package config
+
+import (
+ "encoding/json"
+ "errors"
+ "io/ioutil"
+ "net/url"
+)
+
+type (
+ Action struct {
+ Name string
+ Triggers []Trigger
+ Outputs []Output
+ }
+
+ Trigger struct {
+ URL *url.URL
+ FormValues url.Values
+ }
+
+ OutputKind int
+ Output struct {
+ Kind OutputKind
+ Name string
+
+ URL *url.URL
+ FormValues url.Values
+
+ MQTT *url.URL
+ Value string
+ }
+
+ Config struct {
+ Actions map[string]Action
+ }
+
+ config struct {
+ Actions map[string]struct {
+ Triggers []struct {
+ URL uurl `json:"url"`
+ FormValues map[string]string `json:"formValues"`
+ } `json:"triggers"`
+ Outputs []struct {
+ URL uurl `json:"url"`
+ FormValues map[string]string `json:"formValues"`
+ MQTT uurl `json:"mqtt"`
+ Value string `json:"value"`
+ } `json:"outputs"`
+ } `json:"actions"`
+ }
+
+ uurl struct {
+ *url.URL
+ }
+)
+
+const (
+ HTTPOutput OutputKind = iota
+ MQTTOutput
+)
+
+func ParseFile(path string) (*Config, error) {
+ bytes, err := ioutil.ReadFile(path)
+ if err != nil {
+ return nil, err
+ }
+
+ raw := config{}
+ if err := json.Unmarshal(bytes, &raw); err != nil {
+ return nil, err
+ }
+
+ return configFromConfig(raw)
+}
+
+func configFromConfig(raw config) (*Config, error) {
+ c := &Config{
+ Actions: map[string]Action{},
+ }
+
+ for k, v := range raw.Actions {
+ action := Action{
+ Name: k,
+ }
+
+ for _, rawTrigger := range v.Triggers {
+ trigger := Trigger{
+ URL: rawTrigger.URL.URL,
+ FormValues: urlValuesFromRawValues(rawTrigger.FormValues),
+ }
+ action.Triggers = append(action.Triggers, trigger)
+ }
+ for _, rawOutput := range v.Outputs {
+ output := Output{
+ URL: rawOutput.URL.URL,
+ FormValues: urlValuesFromRawValues(rawOutput.FormValues),
+ MQTT: rawOutput.MQTT.URL,
+ Value: rawOutput.Value,
+ }
+ switch {
+ case output.URL != nil && output.MQTT == nil:
+ output.Kind = HTTPOutput
+ case output.URL == nil && output.MQTT != nil:
+ output.Kind = MQTTOutput
+ default:
+ return nil, errors.New("outputs must be URL xor MQTT")
+ }
+ action.Outputs = append(action.Outputs, output)
+ }
+
+ c.Actions[k] = action
+
+ }
+
+ return c, nil
+}
+
+func (u uurl) MarshalText() ([]byte, error) {
+ return []byte(u.String()), nil
+}
+func (u *uurl) UnmarshalText(raw []byte) error {
+ uu, err := url.Parse(string(raw))
+ if err != nil {
+ return err
+ }
+ u.URL = uu
+ return nil
+}
+
+func urlValuesFromRawValues(raw map[string]string) url.Values {
+ urlValues := map[string][]string{}
+ for k, v := range raw {
+ urlValues[k] = []string{v}
+ }
+ return urlValues
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..a21596f
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,14 @@
+// SPDX-FileCopyrightText: 2020 Ethel Morgan
+//
+// SPDX-License-Identifier: MIT
+
+module go.eth.moe/dispatch
+
+go 1.14
+
+require (
+ github.com/gorilla/mux v1.7.4
+ go.eth.moe/flag v0.0.2
+ go.eth.moe/httputil v0.0.4
+ go.eth.moe/logger v0.0.1
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..ca6280b
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,18 @@
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
+github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
+github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+go.eth.moe/flag v0.0.2 h1:ekP9RsIlkE+1aTH8Fh+Wv+dW9tKH+xFHP/ZDQ6wSpj8=
+go.eth.moe/flag v0.0.2/go.mod h1:z9zTPv9hmk0AGGgZUEOViNlItqiFd2KIexSt/PL96Nw=
+go.eth.moe/httputil v0.0.3 h1:kcE2kXLkqbhcjswCr4kwCB+uo1qgaPY5EyKs2JNzsVw=
+go.eth.moe/httputil v0.0.3/go.mod h1:LyOLlHBzYVOlwFltlTebWcfXJOyxcVkD+BRYADcZ7So=
+go.eth.moe/httputil v0.0.4 h1:OA6XaBId3KePppkIvuPpm3RRL9/2hlq7xlUCkrYjI4Q=
+go.eth.moe/httputil v0.0.4/go.mod h1:LyOLlHBzYVOlwFltlTebWcfXJOyxcVkD+BRYADcZ7So=
+go.eth.moe/logger v0.0.1 h1:ncY0iuVIljShMQtwy+77DvoHDlu6zVZ+7XIT7jyprrY=
+go.eth.moe/logger v0.0.1/go.mod h1:G20TP3ON2S95olTep+qsBSoTfouZeKPukk3Ow42q5OQ=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=