diff options
Diffstat (limited to '')
-rw-r--r-- | .gitignore | 6 | ||||
-rw-r--r-- | .reuse/dep5 | 8 | ||||
-rw-r--r-- | LICENSES/CC0-1.0.txt | 119 | ||||
-rw-r--r-- | LICENSES/MIT.txt | 19 | ||||
-rw-r--r-- | README.md | 56 | ||||
-rw-r--r-- | cmd/dispatch/main.go | 120 | ||||
-rw-r--r-- | config/config.go | 141 | ||||
-rw-r--r-- | go.mod | 14 | ||||
-rw-r--r-- | go.sum | 18 |
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 +} @@ -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 +) @@ -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= |