Skip to content

Commit 7c2f1dc

Browse files
authored
feat(*): add plugins (#264)
1 parent 63d6912 commit 7c2f1dc

File tree

28 files changed

+1023
-136
lines changed

28 files changed

+1023
-136
lines changed

admin/api/sources.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ func (api *API) CreateSource(w http.ResponseWriter, r *http.Request) {
4040
return
4141
}
4242

43+
if source.Type == "http" {
44+
if source.Config.HTTP.Path == "" {
45+
source.Config.HTTP.Path = "/" + utils.UUIDShort()
46+
}
47+
}
48+
4349
source.WorkspaceId = ucontext.GetWorkspaceID(r.Context())
4450
err := api.db.SourcesWS.Insert(r.Context(), &source)
4551
api.assert(err)

db/entities/source.go

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,33 @@ func (m CustomResponse) Value() (driver.Value, error) {
1919
return json.Marshal(m)
2020
}
2121

22+
type SourceConfig struct {
23+
HTTP HttpSourceConfig `json:"http"`
24+
}
25+
26+
func (m *SourceConfig) Scan(src interface{}) error {
27+
return json.Unmarshal(src.([]byte), m)
28+
}
29+
30+
func (m SourceConfig) Value() (driver.Value, error) {
31+
return json.Marshal(m)
32+
}
33+
34+
type HttpSourceConfig struct {
35+
Path string `json:"path"`
36+
Methods Strings `json:"methods"`
37+
Response *CustomResponse `json:"response"`
38+
}
39+
2240
type Source struct {
23-
ID string `json:"id" db:"id"`
24-
Name *string `json:"name" db:"name"`
25-
Enabled bool `json:"enabled" db:"enabled"`
26-
Path string `json:"path" db:"path"`
27-
Methods Strings `json:"methods" db:"methods"`
28-
Async bool `json:"async" db:"async"`
29-
Response *CustomResponse `json:"response" db:"response"`
30-
Metadata Metadata `json:"metadata" db:"metadata"`
31-
RateLimit *RateLimit `json:"rate_limit" yaml:"rate_limit" db:"rate_limit"`
41+
ID string `json:"id" db:"id"`
42+
Name *string `json:"name" db:"name"`
43+
Enabled bool `json:"enabled" db:"enabled"`
44+
Type string `json:"type" db:"type"`
45+
Config SourceConfig `json:"config" db:"config"`
46+
Async bool `json:"async" db:"async"`
47+
Metadata Metadata `json:"metadata" db:"metadata"`
48+
RateLimit *RateLimit `json:"rate_limit" yaml:"rate_limit" db:"rate_limit"`
3249

3350
BaseModel `yaml:"-"`
3451
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
ALTER TABLE IF EXISTS ONLY "sources" ADD COLUMN IF NOT EXISTS "path" TEXT;
2+
ALTER TABLE IF EXISTS ONLY "sources" ADD COLUMN IF NOT EXISTS "methods" TEXT[];
3+
ALTER TABLE IF EXISTS ONLY "sources" ADD COLUMN IF NOT EXISTS "response" JSONB;
4+
5+
UPDATE sources SET
6+
"path" = (config->'http'->>'path'),
7+
"methods" = ARRAY(SELECT jsonb_array_elements_text(config->'http'->'methods')),
8+
"response" = (config->'http'->'response');
9+
10+
ALTER TABLE IF EXISTS ONLY "sources" DROP COLUMN IF EXISTS "type";
11+
ALTER TABLE IF EXISTS ONLY "sources" DROP COLUMN IF EXISTS "config";
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
ALTER TABLE IF EXISTS ONLY "sources" ADD COLUMN IF NOT EXISTS "type" varchar(20);
2+
ALTER TABLE IF EXISTS ONLY "sources" ADD COLUMN IF NOT EXISTS "config" JSONB NOT NULL DEFAULT '{}'::jsonb;
3+
4+
UPDATE sources SET "type" = 'http', "config" = jsonb_build_object('http', jsonb_build_object('methods', methods, 'path', path, 'response', response));
5+
6+
ALTER TABLE IF EXISTS ONLY "sources" DROP COLUMN IF EXISTS "path";
7+
ALTER TABLE IF EXISTS ONLY "sources" DROP COLUMN IF EXISTS "methods";
8+
ALTER TABLE IF EXISTS ONLY "sources" DROP COLUMN IF EXISTS "response";

examples/function/webhookx-function-sample.yml

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@ endpoints:
1414

1515
sources:
1616
- name: github-source
17-
path: /github
18-
methods: [ "POST" ]
19-
response:
20-
code: 200
21-
content_type: application/json
22-
body: '{"message": "OK"}'
17+
type: http
18+
config:
19+
http:
20+
path: /github
21+
methods: [ "POST" ]
22+
response:
23+
code: 200
24+
content_type: application/json
25+
body: '{"message": "OK"}'
2326
plugins:
2427
- name: function
2528
config:

openapi.yml

Lines changed: 107 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -877,33 +877,21 @@ components:
877877
enabled:
878878
type: boolean
879879
default: true
880-
path:
880+
type:
881881
type: string
882-
methods:
883-
type: array
884-
items:
885-
type: string
886-
enum: [ GET, POST, PUT, DELETE, PATCH ]
887-
minItems: 1
882+
enum: [ "http" ]
883+
default: http
884+
config:
885+
type: object
886+
properties:
887+
http:
888+
$ref: "#/components/schemas/HTTPSourceConfig"
889+
required:
890+
- http
888891
async:
889892
type: boolean
890893
description: "Whether to ingest events asynchronously through the queue"
891894
default: false
892-
response:
893-
type: object
894-
nullable: true
895-
properties:
896-
code:
897-
type: integer
898-
minimum: 200
899-
maximum: 599
900-
content_type:
901-
type: string
902-
body:
903-
type: string
904-
required:
905-
- code
906-
- content_type
907895
metadata:
908896
$ref: "#/components/schemas/Metadata"
909897
rate_limit:
@@ -915,8 +903,7 @@ components:
915903
type: integer
916904
readOnly: true
917905
required:
918-
- path
919-
- methods
906+
- config
920907

921908
Plugin:
922909
type: object
@@ -1049,3 +1036,99 @@ components:
10491036
type: string
10501037
format: jsonschema
10511038
maxLength: 1048576
1039+
1040+
BasicAuthPluginConfiguration:
1041+
description: "The basic auth plugin configuration"
1042+
type: object
1043+
properties:
1044+
username:
1045+
description: "The username used for Basic Authentication"
1046+
type: string
1047+
password:
1048+
description: "The password used for Basic Authentication"
1049+
type: string
1050+
required:
1051+
- username
1052+
- password
1053+
1054+
KeyAuthPluginConfiguration:
1055+
description: "The key-auth plugin configuration"
1056+
type: object
1057+
properties:
1058+
param_name:
1059+
description: "The parameter name of api key."
1060+
type: string
1061+
minLength: 1
1062+
param_locations:
1063+
description: "The locations where the api key can be provided."
1064+
type: array
1065+
uniqueItems: true
1066+
minItems: 1
1067+
items:
1068+
type: string
1069+
enum: [ "header", "query" ]
1070+
key:
1071+
description: "The api key used for authentication"
1072+
type: string
1073+
minLength: 1
1074+
required:
1075+
- param_name
1076+
- param_locations
1077+
- key
1078+
1079+
HmacAuthPluginConfiguration:
1080+
description: "The hmac-auth plugin configuration"
1081+
type: object
1082+
properties:
1083+
hash:
1084+
description: "The hash algorithm used to generate the HMAC signature."
1085+
type: string
1086+
enum: [ "md5", "sha-1", "sha-256", "sha-512" ]
1087+
default: "sha-256"
1088+
encoding:
1089+
description: "The encoding format of the generated signature."
1090+
type: string
1091+
enum: [ "hex", "base64", "base64url" ]
1092+
default: "hex"
1093+
signature_header:
1094+
description: "The HTTP header name where the HMAC signature is sent."
1095+
type: string
1096+
minLength: 1
1097+
secret:
1098+
type: string
1099+
minLength: 1
1100+
required:
1101+
- hash
1102+
- encoding
1103+
- signature_header
1104+
- secret
1105+
1106+
HTTPSourceConfig:
1107+
type: object
1108+
default:
1109+
methods: [ "POST" ]
1110+
properties:
1111+
path:
1112+
type: string
1113+
methods:
1114+
type: array
1115+
items:
1116+
type: string
1117+
enum: [ GET, POST, PUT, DELETE, PATCH ]
1118+
minItems: 1
1119+
default: [ "POST" ]
1120+
response:
1121+
type: object
1122+
nullable: true
1123+
properties:
1124+
code:
1125+
type: integer
1126+
minimum: 200
1127+
maximum: 599
1128+
content_type:
1129+
type: string
1130+
body:
1131+
type: string
1132+
required:
1133+
- code
1134+
- content_type

plugins/basic-auth/plugin.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package basic_auth
2+
3+
import (
4+
"context"
5+
6+
"github.com/getkin/kin-openapi/openapi3"
7+
"github.com/webhookx-io/webhookx/db/entities"
8+
"github.com/webhookx-io/webhookx/pkg/http/response"
9+
"github.com/webhookx-io/webhookx/pkg/plugin"
10+
)
11+
12+
type Config struct {
13+
Username string `json:"username"`
14+
Password string `json:"password"`
15+
}
16+
17+
func (c Config) Schema() *openapi3.Schema {
18+
return entities.LookupSchema("BasicAuthPluginConfiguration")
19+
}
20+
21+
type BasicAuthPlugin struct {
22+
plugin.BasePlugin[Config]
23+
}
24+
25+
func (p *BasicAuthPlugin) Name() string {
26+
return "basic-auth"
27+
}
28+
29+
func (p *BasicAuthPlugin) ExecuteInbound(ctx context.Context, inbound *plugin.Inbound) (result plugin.InboundResult, err error) {
30+
username, password, ok := inbound.Request.BasicAuth()
31+
if !ok || username != p.Config.Username || password != p.Config.Password {
32+
response.JSON(inbound.Response, 401, `{"message":"Unauthorized"}`)
33+
result.Terminated = true
34+
}
35+
36+
result.Payload = inbound.RawBody
37+
return
38+
}

plugins/hmac-auth/plugin.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package hmac_auth
2+
3+
import (
4+
"context"
5+
"crypto/hmac"
6+
"crypto/md5"
7+
"crypto/sha1"
8+
"crypto/sha256"
9+
"crypto/sha512"
10+
"crypto/subtle"
11+
"encoding/base64"
12+
"encoding/hex"
13+
"errors"
14+
"fmt"
15+
"hash"
16+
17+
"github.com/getkin/kin-openapi/openapi3"
18+
"github.com/webhookx-io/webhookx/db/entities"
19+
"github.com/webhookx-io/webhookx/pkg/http/response"
20+
"github.com/webhookx-io/webhookx/pkg/plugin"
21+
)
22+
23+
var (
24+
ErrInvalidHashMethod = errors.New("invalid hash method")
25+
ErrInvalidEncodingMethod = errors.New("invalid encoding method")
26+
)
27+
28+
var hashes = map[string]func() hash.Hash{
29+
"md5": md5.New,
30+
"sha-1": sha1.New,
31+
"sha-256": sha256.New,
32+
"sha-512": sha512.New,
33+
}
34+
35+
func Hmac(algorithm string, key string, data string) []byte {
36+
fn, ok := hashes[algorithm]
37+
if !ok {
38+
panic(fmt.Errorf("%w: %s", ErrInvalidHashMethod, algorithm))
39+
}
40+
h := hmac.New(fn, []byte(key))
41+
h.Write([]byte(data))
42+
return h.Sum(nil)
43+
}
44+
45+
func encode(encoding string, data []byte) string {
46+
switch encoding {
47+
case "hex":
48+
return hex.EncodeToString(data)
49+
case "base64":
50+
return base64.StdEncoding.EncodeToString(data)
51+
case "base64url":
52+
return base64.RawURLEncoding.EncodeToString(data)
53+
default:
54+
panic(fmt.Errorf("%w: %s", ErrInvalidEncodingMethod, encoding))
55+
}
56+
}
57+
58+
type Config struct {
59+
Hash string `json:"hash"`
60+
Encoding string `json:"encoding"`
61+
SignatureHeader string `json:"signature_header"`
62+
Secret string `json:"secret"`
63+
}
64+
65+
func (c Config) Schema() *openapi3.Schema {
66+
return entities.LookupSchema("HmacAuthPluginConfiguration")
67+
}
68+
69+
type HmacAuthPlugin struct {
70+
plugin.BasePlugin[Config]
71+
}
72+
73+
func (p *HmacAuthPlugin) Name() string {
74+
return "hmac-auth"
75+
}
76+
77+
func (p *HmacAuthPlugin) ExecuteInbound(ctx context.Context, inbound *plugin.Inbound) (result plugin.InboundResult, err error) {
78+
matched := false
79+
signature := inbound.Request.Header.Get(p.Config.SignatureHeader)
80+
if len(signature) > 0 {
81+
bytes := Hmac(p.Config.Hash, p.Config.Secret, string(inbound.RawBody))
82+
expectedSignature := encode(p.Config.Encoding, bytes)
83+
matched = subtle.ConstantTimeCompare([]byte(signature), []byte(expectedSignature)) == 1
84+
}
85+
86+
if !matched {
87+
response.JSON(inbound.Response, 401, `{"message":"Unauthorized"}`)
88+
result.Terminated = true
89+
}
90+
result.Payload = inbound.RawBody
91+
92+
return
93+
}

0 commit comments

Comments
 (0)