Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement call bouncer #7

Merged
merged 4 commits into from
Aug 4, 2024
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
27 changes: 27 additions & 0 deletions .github/workflows/qa.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,33 @@ jobs:
run: sqlc vet
- name: Diff
run: sqlc diff
dbmate:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_USER: pbx
POSTGRES_DB: asterisk
POSTGRES_PASSWORD: pbx
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
DATABASE_URL: postgres://pbx:[email protected]:5432/asterisk?sslmode=disable
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dbmate
run: npm install dbmate
- name: Run migrations
run: npx dbmate up
docker:
runs-on: ubuntu-latest
steps:
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ docs:
swag init --generalInfo cmd/main.go --outputTypes=yaml

swagger:
docker run --detach -p 4000:8080 -e API_URL=/doc/swagger.yaml --mount 'type=bind,src=$(shell pwd)/docs,dst=/usr/share/nginx/html/doc' swaggerapi/swagger-ui
docker run --detach --name eryth-swagger -p 4000:8080 -e API_URL=/doc/swagger.yaml --mount 'type=bind,src=$(shell pwd)/docs,dst=/usr/share/nginx/html/doc' swaggerapi/swagger-ui
5 changes: 5 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"context"
"fmt"
"github.com/crazybolillo/eryth/internal/bouncer"
"github.com/crazybolillo/eryth/internal/handler"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
Expand Down Expand Up @@ -75,6 +76,10 @@ func serve(ctx context.Context) error {
endpoint := handler.Endpoint{Conn: conn}
r.Mount("/endpoint", endpoint.Router())

checker := &bouncer.Bouncer{Conn: conn}
authorization := handler.Authorization{Bouncer: checker}
r.Mount("/bouncer", authorization.Router())

slog.Info("Listening on :8080")
return http.ListenAndServe(":8080", r)
}
2 changes: 1 addition & 1 deletion db/migrations/20240730043324_asterisk_v21_3_1.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
-- migrate:upclear
-- migrate:up
CREATE TYPE public.ast_bool_values AS ENUM (
'0',
'1',
Expand Down
12 changes: 12 additions & 0 deletions db/migrations/20240730051335_ery_extension.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- migrate:up
ALTER TABLE ps_endpoints ADD COLUMN sid SERIAL PRIMARY KEY;

CREATE TABLE ery_extension (
id SERIAL PRIMARY KEY,
endpoint_id SERIAL NOT NULL,
extension varchar UNIQUE,
FOREIGN KEY (endpoint_id) REFERENCES ps_endpoints(sid)
)

-- migrate:down

56 changes: 51 additions & 5 deletions docs/swagger.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
definitions:
bouncer.Response:
properties:
allow:
type: boolean
destination:
type: string
type: object
handler.AuthorizationRequest:
properties:
endpoint:
type: string
extension:
type: string
type: object
handler.createEndpointRequest:
properties:
codecs:
Expand All @@ -7,6 +21,8 @@ definitions:
type: array
context:
type: string
extension:
type: string
id:
type: string
max_contacts:
Expand All @@ -18,13 +34,20 @@ definitions:
transport:
type: string
type: object
handler.listEndpointsRequest:
properties:
endpoints:
items:
$ref: '#/definitions/sqlc.ListEndpointsRow'
type: array
type: object
sqlc.ListEndpointsRow:
properties:
context:
type: string
id:
extension:
type: string
transport:
id:
type: string
type: object
host: localhost:8080
Expand All @@ -34,6 +57,31 @@ info:
title: Asterisk Administration API
version: "1.0"
paths:
/bouncer:
post:
consumes:
- application/json
parameters:
- description: Action to be reviewed
in: body
name: payload
required: true
schema:
$ref: '#/definitions/handler.AuthorizationRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/bouncer.Response'
"400":
description: Bad Request
"500":
description: Internal Server Error
summary: Determine whether the specified action (call) is allowed or not.
tags:
- bouncer
/endpoint:
post:
consumes:
Expand Down Expand Up @@ -87,9 +135,7 @@ paths:
"200":
description: OK
schema:
items:
$ref: '#/definitions/sqlc.ListEndpointsRow'
type: array
$ref: '#/definitions/handler.listEndpointsRequest'
"400":
description: Bad Request
"500":
Expand Down
43 changes: 43 additions & 0 deletions internal/bouncer/bouncer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package bouncer

import (
"context"
"github.com/crazybolillo/eryth/internal/db"
"github.com/crazybolillo/eryth/internal/sqlc"
"github.com/jackc/pgx/v5"
"log/slog"
)

type Response struct {
Allow bool `json:"allow"`
Destination string `json:"destination"`
}

type Bouncer struct {
*pgx.Conn
}

func (b *Bouncer) Check(ctx context.Context, endpoint, dialed string) Response {
result := Response{
Allow: false,
Destination: "",
}

tx, err := b.Begin(ctx)
if err != nil {
slog.Error("Unable to start transaction", slog.String("reason", err.Error()))
return result
}

queries := sqlc.New(tx)
destination, err := queries.GetEndpointByExtension(ctx, db.Text(dialed))
if err != nil {
slog.Error("Failed to retrieve endpoint", slog.String("dialed", dialed), slog.String("reason", err.Error()))
return result
}

return Response{
Allow: true,
Destination: destination,
}
}
63 changes: 63 additions & 0 deletions internal/handler/authorization.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package handler

import (
"context"
"encoding/json"
"github.com/crazybolillo/eryth/internal/bouncer"
"github.com/go-chi/chi/v5"
"log/slog"
"net/http"
)

type CallBouncer interface {
Check(ctx context.Context, endpoint, dialed string) bouncer.Response
}

type Authorization struct {
Bouncer CallBouncer
}

type AuthorizationRequest struct {
From string `json:"endpoint"`
Extension string `json:"extension"`
}

func (e *Authorization) Router() chi.Router {
r := chi.NewRouter()
r.Post("/", e.post)

return r
}

// @Summary Determine whether the specified action (call) is allowed or not.
// @Accept json
// @Produce json
// @Param payload body AuthorizationRequest true "Action to be reviewed"
// @Success 200 {object} bouncer.Response
// @Failure 400
// @Failure 500
// @Tags bouncer
// @Router /bouncer [post]
func (e *Authorization) post(w http.ResponseWriter, r *http.Request) {
var payload AuthorizationRequest
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&payload)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}

response := e.Bouncer.Check(r.Context(), payload.From, payload.Extension)
content, err := json.Marshal(response)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
_, err = w.Write(content)
if err != nil {
slog.Error("Failed to write response", slog.String("path", r.URL.Path), slog.String("reason", err.Error()))
}
w.WriteHeader(http.StatusOK)
}
31 changes: 27 additions & 4 deletions internal/handler/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,15 @@ type createEndpointRequest struct {
ID string `json:"id"`
Password string `json:"password"`
Realm string `json:"realm,omitempty"`
Transport string `json:"transport"`
Transport string `json:"transport,omitempty"`
Context string `json:"context"`
Codecs []string `json:"codecs"`
MaxContacts int32 `json:"max_contacts,omitempty"`
Extension string `json:"extension,omitempty"`
}

type listEndpointsRequest struct {
Endpoints []sqlc.ListEndpointsRow `json:"endpoints"`
}

func (e *Endpoint) Router() chi.Router {
Expand All @@ -40,7 +45,7 @@ func (e *Endpoint) Router() chi.Router {
// @Summary List existing endpoints.
// @Param limit query int false "Limit the amount of endpoints returned" default(15)
// @Produce json
// @Success 200 {object} []sqlc.ListEndpointsRow
// @Success 200 {object} listEndpointsRequest
// @Failure 400
// @Failure 500
// @Tags endpoints
Expand All @@ -60,11 +65,18 @@ func (e *Endpoint) list(w http.ResponseWriter, r *http.Request) {
queries := sqlc.New(e.Conn)
endpoints, err := queries.ListEndpoints(r.Context(), int32(limit))
if err != nil {
slog.Error("Query execution failed", slog.String("path", r.URL.Path), slog.String("msg", err.Error()))
w.WriteHeader(http.StatusInternalServerError)
return
}
if endpoints == nil {
endpoints = []sqlc.ListEndpointsRow{}
}

content, err := json.Marshal(endpoints)
response := listEndpointsRequest{
Endpoints: endpoints,
}
content, err := json.Marshal(response)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
Expand Down Expand Up @@ -120,7 +132,7 @@ func (e *Endpoint) create(w http.ResponseWriter, r *http.Request) {
return
}

err = queries.NewEndpoint(r.Context(), sqlc.NewEndpointParams{
sid, err := queries.NewEndpoint(r.Context(), sqlc.NewEndpointParams{
ID: payload.ID,
Transport: db.Text(payload.Transport),
Context: db.Text(payload.Context),
Expand All @@ -140,6 +152,17 @@ func (e *Endpoint) create(w http.ResponseWriter, r *http.Request) {
return
}

if payload.Extension != "" {
err = queries.NewExtension(r.Context(), sqlc.NewExtensionParams{
EndpointID: sid,
Extension: db.Text(payload.Extension),
})
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
}

err = tx.Commit(r.Context())
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
Expand Down
7 changes: 7 additions & 0 deletions internal/sqlc/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading