Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions admin/api/sources.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ func (api *API) CreateSource(w http.ResponseWriter, r *http.Request) {
return
}

if source.Type == "http" {
if source.Config.HTTP.Path == "" {
source.Config.HTTP.Path = "/" + utils.UUIDShort()
}
}

source.WorkspaceId = ucontext.GetWorkspaceID(r.Context())
err := api.db.SourcesWS.Insert(r.Context(), &source)
api.assert(err)
Expand Down
35 changes: 26 additions & 9 deletions db/entities/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,33 @@ func (m CustomResponse) Value() (driver.Value, error) {
return json.Marshal(m)
}

type SourceConfig struct {
HTTP HttpSourceConfig `json:"http"`
}

func (m *SourceConfig) Scan(src interface{}) error {
return json.Unmarshal(src.([]byte), m)
}

func (m SourceConfig) Value() (driver.Value, error) {
return json.Marshal(m)
}

type HttpSourceConfig struct {
Path string `json:"path"`
Methods Strings `json:"methods"`
Response *CustomResponse `json:"response"`
}

type Source struct {
ID string `json:"id" db:"id"`
Name *string `json:"name" db:"name"`
Enabled bool `json:"enabled" db:"enabled"`
Path string `json:"path" db:"path"`
Methods Strings `json:"methods" db:"methods"`
Async bool `json:"async" db:"async"`
Response *CustomResponse `json:"response" db:"response"`
Metadata Metadata `json:"metadata" db:"metadata"`
RateLimit *RateLimit `json:"rate_limit" yaml:"rate_limit" db:"rate_limit"`
ID string `json:"id" db:"id"`
Name *string `json:"name" db:"name"`
Enabled bool `json:"enabled" db:"enabled"`
Type string `json:"type" db:"type"`
Config SourceConfig `json:"config" db:"config"`
Async bool `json:"async" db:"async"`
Metadata Metadata `json:"metadata" db:"metadata"`
RateLimit *RateLimit `json:"rate_limit" yaml:"rate_limit" db:"rate_limit"`

BaseModel `yaml:"-"`
}
Expand Down
11 changes: 11 additions & 0 deletions db/migrations/1762423418_source_config.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
ALTER TABLE IF EXISTS ONLY "sources" ADD COLUMN IF NOT EXISTS "path" TEXT;
ALTER TABLE IF EXISTS ONLY "sources" ADD COLUMN IF NOT EXISTS "methods" TEXT[];
ALTER TABLE IF EXISTS ONLY "sources" ADD COLUMN IF NOT EXISTS "response" JSONB;

UPDATE sources SET
"path" = (config->'http'->>'path'),
"methods" = ARRAY(SELECT jsonb_array_elements_text(config->'http'->'methods')),
"response" = (config->'http'->'response');

ALTER TABLE IF EXISTS ONLY "sources" DROP COLUMN IF EXISTS "type";
ALTER TABLE IF EXISTS ONLY "sources" DROP COLUMN IF EXISTS "config";
8 changes: 8 additions & 0 deletions db/migrations/1762423418_source_config.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
ALTER TABLE IF EXISTS ONLY "sources" ADD COLUMN IF NOT EXISTS "type" varchar(20);
ALTER TABLE IF EXISTS ONLY "sources" ADD COLUMN IF NOT EXISTS "config" JSONB NOT NULL DEFAULT '{}'::jsonb;

UPDATE sources SET "type" = 'http', "config" = jsonb_build_object('http', jsonb_build_object('methods', methods, 'path', path, 'response', response));

ALTER TABLE IF EXISTS ONLY "sources" DROP COLUMN IF EXISTS "path";
ALTER TABLE IF EXISTS ONLY "sources" DROP COLUMN IF EXISTS "methods";
ALTER TABLE IF EXISTS ONLY "sources" DROP COLUMN IF EXISTS "response";
15 changes: 9 additions & 6 deletions examples/function/webhookx-function-sample.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@ endpoints:

sources:
- name: github-source
path: /github
methods: [ "POST" ]
response:
code: 200
content_type: application/json
body: '{"message": "OK"}'
type: http
config:
http:
path: /github
methods: [ "POST" ]
response:
code: 200
content_type: application/json
body: '{"message": "OK"}'
plugins:
- name: function
config:
Expand Down
131 changes: 107 additions & 24 deletions openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -877,33 +877,21 @@ components:
enabled:
type: boolean
default: true
path:
type:
type: string
methods:
type: array
items:
type: string
enum: [ GET, POST, PUT, DELETE, PATCH ]
minItems: 1
enum: [ "http" ]
default: http
config:
type: object
properties:
http:
$ref: "#/components/schemas/HTTPSourceConfig"
required:
- http
async:
type: boolean
description: "Whether to ingest events asynchronously through the queue"
default: false
response:
type: object
nullable: true
properties:
code:
type: integer
minimum: 200
maximum: 599
content_type:
type: string
body:
type: string
required:
- code
- content_type
metadata:
$ref: "#/components/schemas/Metadata"
rate_limit:
Expand All @@ -915,8 +903,7 @@ components:
type: integer
readOnly: true
required:
- path
- methods
- config

Plugin:
type: object
Expand Down Expand Up @@ -1049,3 +1036,99 @@ components:
type: string
format: jsonschema
maxLength: 1048576

BasicAuthPluginConfiguration:
description: "The basic auth plugin configuration"
type: object
properties:
username:
description: "The username used for Basic Authentication"
type: string
password:
description: "The password used for Basic Authentication"
type: string
required:
- username
- password

KeyAuthPluginConfiguration:
description: "The key-auth plugin configuration"
type: object
properties:
param_name:
description: "The parameter name of api key."
type: string
minLength: 1
param_locations:
description: "The locations where the api key can be provided."
type: array
uniqueItems: true
minItems: 1
items:
type: string
enum: [ "header", "query" ]
key:
description: "The api key used for authentication"
type: string
minLength: 1
required:
- param_name
- param_locations
- key

HmacAuthPluginConfiguration:
description: "The hmac-auth plugin configuration"
type: object
properties:
hash:
description: "The hash algorithm used to generate the HMAC signature."
type: string
enum: [ "md5", "sha-1", "sha-256", "sha-512" ]
default: "sha-256"
encoding:
description: "The encoding format of the generated signature."
type: string
enum: [ "hex", "base64", "base64url" ]
default: "hex"
signature_header:
description: "The HTTP header name where the HMAC signature is sent."
type: string
minLength: 1
secret:
type: string
minLength: 1
required:
- hash
- encoding
- signature_header
- secret

HTTPSourceConfig:
type: object
default:
methods: [ "POST" ]
properties:
path:
type: string
methods:
type: array
items:
type: string
enum: [ GET, POST, PUT, DELETE, PATCH ]
minItems: 1
default: [ "POST" ]
response:
type: object
nullable: true
properties:
code:
type: integer
minimum: 200
maximum: 599
content_type:
type: string
body:
type: string
required:
- code
- content_type
38 changes: 38 additions & 0 deletions plugins/basic-auth/plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package basic_auth

import (
"context"

"github.com/getkin/kin-openapi/openapi3"
"github.com/webhookx-io/webhookx/db/entities"
"github.com/webhookx-io/webhookx/pkg/http/response"
"github.com/webhookx-io/webhookx/pkg/plugin"
)

type Config struct {
Username string `json:"username"`
Password string `json:"password"`
}

func (c Config) Schema() *openapi3.Schema {
return entities.LookupSchema("BasicAuthPluginConfiguration")
}

type BasicAuthPlugin struct {
plugin.BasePlugin[Config]
}

func (p *BasicAuthPlugin) Name() string {
return "basic-auth"
}

func (p *BasicAuthPlugin) ExecuteInbound(ctx context.Context, inbound *plugin.Inbound) (result plugin.InboundResult, err error) {
username, password, ok := inbound.Request.BasicAuth()
if !ok || username != p.Config.Username || password != p.Config.Password {
response.JSON(inbound.Response, 401, `{"message":"Unauthorized"}`)
result.Terminated = true
}

result.Payload = inbound.RawBody
return
}
93 changes: 93 additions & 0 deletions plugins/hmac-auth/plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package hmac_auth

import (
"context"
"crypto/hmac"
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"crypto/subtle"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"hash"

"github.com/getkin/kin-openapi/openapi3"
"github.com/webhookx-io/webhookx/db/entities"
"github.com/webhookx-io/webhookx/pkg/http/response"
"github.com/webhookx-io/webhookx/pkg/plugin"
)

var (
ErrInvalidHashMethod = errors.New("invalid hash method")
ErrInvalidEncodingMethod = errors.New("invalid encoding method")
)

var hashes = map[string]func() hash.Hash{
"md5": md5.New,
"sha-1": sha1.New,
"sha-256": sha256.New,
"sha-512": sha512.New,
}

func Hmac(algorithm string, key string, data string) []byte {
fn, ok := hashes[algorithm]
if !ok {
panic(fmt.Errorf("%w: %s", ErrInvalidHashMethod, algorithm))
}
h := hmac.New(fn, []byte(key))
h.Write([]byte(data))
return h.Sum(nil)
}

func encode(encoding string, data []byte) string {
switch encoding {
case "hex":
return hex.EncodeToString(data)
case "base64":
return base64.StdEncoding.EncodeToString(data)
case "base64url":
return base64.RawURLEncoding.EncodeToString(data)
default:
panic(fmt.Errorf("%w: %s", ErrInvalidEncodingMethod, encoding))
}
}

type Config struct {
Hash string `json:"hash"`
Encoding string `json:"encoding"`
SignatureHeader string `json:"signature_header"`
Secret string `json:"secret"`
}

func (c Config) Schema() *openapi3.Schema {
return entities.LookupSchema("HmacAuthPluginConfiguration")
}

type HmacAuthPlugin struct {
plugin.BasePlugin[Config]
}

func (p *HmacAuthPlugin) Name() string {
return "hmac-auth"
}

func (p *HmacAuthPlugin) ExecuteInbound(ctx context.Context, inbound *plugin.Inbound) (result plugin.InboundResult, err error) {
matched := false
signature := inbound.Request.Header.Get(p.Config.SignatureHeader)
if len(signature) > 0 {
bytes := Hmac(p.Config.Hash, p.Config.Secret, string(inbound.RawBody))
expectedSignature := encode(p.Config.Encoding, bytes)
matched = subtle.ConstantTimeCompare([]byte(signature), []byte(expectedSignature)) == 1
}

if !matched {
response.JSON(inbound.Response, 401, `{"message":"Unauthorized"}`)
result.Terminated = true
}
result.Payload = inbound.RawBody

return
}
Loading
Loading