Skip to content

Commit

Permalink
add reverse proxy (#492)
Browse files Browse the repository at this point in the history
* add reverse proxy

* add feature flag to enable reverse proxy

* update version

* delete bundlex folder in Dockerfile

* refactor timestamp adjuster

* fix video assembler

* upgrade deps
  • Loading branch information
gBillal authored Nov 6, 2024
1 parent 3605af6 commit 1f6dcec
Show file tree
Hide file tree
Showing 20 changed files with 161 additions and 43 deletions.
1 change: 1 addition & 0 deletions .github/workflows/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ COPY config config
COPY assets assets
COPY apps apps
COPY rel rel
RUN rm -rf apps/ex_nvr/priv/bundlex

RUN mix deps.get
RUN mix deps.compile
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ If you want to configure some aspects of `ex_nvr`, you can set the following env
| EXNVR_SSL_KEY_PATH | The path to the SSL key. |
| EXNVR_SSL_CERT_PATH | The path to the SSL certificate. |
| EXNVR_JSON_LOGGER | Enable json logging, defaults to: `true` |
| ENABLE_REVERSE_PROXY | Enable reverse proxy. All endpoint calls to a path that starts with `/service/{ipv4}` will be proxied to `http://{ipv4}`. `ipv4` is a valid private ip address. Defaults to: `false` |

## WebRTC

Expand Down
9 changes: 7 additions & 2 deletions apps/ex_nvr/c_src/ex_nvr/video_assembler.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ UNIFEX_TERM assemble_recordings(UnifexEnv *env, recording *recordings, unsigned
int64_t last_dts = -1;
int64_t duration = 0, offset = 0;
AVRational time_base = {1, 1};
AVRational target_time_base;

// init write context
if (avformat_alloc_output_context2(&write_ctx, NULL, "mp4", NULL) < 0)
Expand Down Expand Up @@ -78,6 +79,9 @@ UNIFEX_TERM assemble_recordings(UnifexEnv *env, recording *recordings, unsigned
res = assemble_recordings_result_error(env, "write_header");
goto exit_assemble_files;
}

// time_base of the writer stream may be updated by the muxer
target_time_base = write_ctx->streams[0]->time_base;
}

av_read_frame(read_ctx, packet);
Expand All @@ -95,8 +99,9 @@ UNIFEX_TERM assemble_recordings(UnifexEnv *env, recording *recordings, unsigned

do
{
packet->dts += last_dts;
packet->pts += last_dts;
packet->dts = av_rescale_q(packet->dts + last_dts, time_base, target_time_base);
packet->pts = av_rescale_q(packet->pts + last_dts, time_base, target_time_base);

duration += packet->duration;
recording_duration += packet->duration;

Expand Down
37 changes: 23 additions & 14 deletions apps/ex_nvr/lib/ex_nvr/pipeline/output/hls/timestamp_adjuster.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,39 @@ defmodule ExNVR.Pipeline.Output.HLS.TimestampAdjuster do

use Membrane.Filter

alias ExNVR.Pipeline.Event.StreamClosed
alias Membrane.Buffer
alias Membrane.Time

def_input_pad :input, accepted_format: _any
def_output_pad :output, accepted_format: _any

@impl true
def handle_init(_ctx, _opts) do
{[], %{last_buffer_timestamp: 0, offset: 0}}
{[], %{last_buffer_timestamp: 0, offset: 0, adjust_offset: false}}
end

@impl true
def handle_buffer(_pad, buffer, _ctx, %{adjust_offset: true} = state) do
offset =
max(
0,
state.last_buffer_timestamp - Buffer.get_dts_or_pts(buffer) + Time.milliseconds(30)
)

do_handle_buffer(buffer, %{state | adjust_offset: false, offset: offset})
end

@impl true
def handle_buffer(_pad, buffer, _ctx, state) do
do_handle_buffer(buffer, state)
end

@impl true
def handle_event(_pad, event, _ctx, state) do
{[event: {:output, event}], %{state | adjust_offset: true}}
end

defp do_handle_buffer(buffer, state) do
buffer = %{
buffer
| dts: buffer.dts && buffer.dts + state.offset,
Expand All @@ -30,16 +51,4 @@ defmodule ExNVR.Pipeline.Output.HLS.TimestampAdjuster do

{[buffer: {:output, buffer}], state}
end

@impl true
def handle_event(_pad, %StreamClosed{}, _ctx, state) do
# put a 30 millisecond as the duration of the last frame
offset = state.last_buffer_timestamp + Membrane.Time.milliseconds(30)
{[], %{last_buffer_timestamp: 0, offset: offset}}
end

@impl true
def handle_event(pad, event, ctx, state) do
super(pad, event, ctx, state)
end
end
4 changes: 2 additions & 2 deletions apps/ex_nvr/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule ExNVR.MixProject do
def project do
[
app: :ex_nvr,
version: "0.15.2",
version: "0.16.0",
build_path: "../../_build",
config_path: "../../config/config.exs",
deps_path: "../../deps",
Expand Down Expand Up @@ -37,7 +37,7 @@ defmodule ExNVR.MixProject do
{:ex_nvr_rtsp, in_umbrella: true},
{:ex_sdp, "~> 1.0", override: true},
{:unifex, "~> 1.1"},
{:bundlex, "~> 1.4.6"},
{:bundlex, "~> 1.5", override: true},
{:bcrypt_elixir, "~> 3.0"},
{:phoenix_pubsub, "~> 2.1"},
{:ecto_sql, "~> 3.6"},
Expand Down
2 changes: 1 addition & 1 deletion apps/ex_nvr_rtsp/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule ExNvrRtsp.MixProject do
def project do
[
app: :ex_nvr_rtsp,
version: "0.15.2",
version: "0.16.0",
build_path: "../../_build",
config_path: "../../config/config.exs",
deps_path: "../../deps",
Expand Down
10 changes: 5 additions & 5 deletions apps/ex_nvr_web/assets/package-lock.json

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

2 changes: 1 addition & 1 deletion apps/ex_nvr_web/assets/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ex_nvr",
"version": "0.15.2",
"version": "0.16.0",
"description": "",
"main": "index.js",
"scripts": {
Expand Down
5 changes: 0 additions & 5 deletions apps/ex_nvr_web/lib/ex_nvr_web/endpoint.ex
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,6 @@ defmodule ExNVRWeb.Endpoint do
plug Plug.RequestId
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]

plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Phoenix.json_library()

plug Plug.MethodOverride
plug Plug.Head
plug Plug.Session, @session_options
Expand Down
4 changes: 1 addition & 3 deletions apps/ex_nvr_web/lib/ex_nvr_web/plugs/device.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@ defmodule ExNVRWeb.Plug.Device do
Conn.assign(conn, :device, device)

nil ->
conn
|> not_found()
|> Conn.halt()
not_found(conn)
end
end
end
17 changes: 17 additions & 0 deletions apps/ex_nvr_web/lib/ex_nvr_web/plugs/proxy_allow.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
defmodule ExNVRWeb.Plug.ProxyAllow do
@moduledoc """
Check if reverse proxy requests are allowed
"""

import ExNVRWeb.Controller.Helpers

alias Plug.Conn

def init(opts), do: opts

def call(%Conn{} = conn, _opts) do
if Application.get_env(:ex_nvr_web, :enable_reverse_proxy, false),
do: conn,
else: not_found(conn)
end
end
21 changes: 21 additions & 0 deletions apps/ex_nvr_web/lib/ex_nvr_web/plugs/proxy_path_rewriter.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
defmodule ExNVRWeb.Plug.ProxyPathRewriter do
@moduledoc false

alias Plug.Conn

def init(opts), do: opts

def call(%Conn{} = conn, _opts) do
case Enum.count(conn.path_info) < 2 do
true ->
conn

false ->
{upstream, path_infos} = List.pop_at(conn.path_info, 1)

conn
|> Conn.put_req_header("x-host", upstream)
|> Map.put(:path_info, path_infos)
end
end
end
40 changes: 40 additions & 0 deletions apps/ex_nvr_web/lib/ex_nvr_web/reverse_proxy.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
defmodule ExNVRWeb.ReverseProxy do
@moduledoc false

require Logger

import Plug.Conn

@private_ips [
{{10, 0, 0, 0}, {10, 255, 255, 255}},
{{172, 16, 0, 0}, {172, 31, 255, 255}},
{{192, 168, 0, 0}, {192, 168, 255, 255}}
]

def reverse_proxy(conn) do
%URI{scheme: get_scheme(conn), host: validate_host(conn)} |> URI.to_string()
end

def handle_error(error, conn) do
Logger.error("error occurred while trying to reverse proxy request: #{inspect(error)}")
send_resp(conn, 500, "Internal Server Error")
end

defp validate_host(conn) do
with [addr] <- get_req_header(conn, "x-host"),
{:ok, ip_addr} <- :inet.parse_ipv4_address(to_charlist(addr)),
true <- Enum.any?(@private_ips, &(elem(&1, 0) < ip_addr and elem(&1, 1) > ip_addr)) do
:inet.ntoa(ip_addr) |> to_string()
else
_reason ->
UUID.uuid4()
end
end

defp get_scheme(conn) do
case get_req_header(conn, "x-scheme") do
[scheme] when scheme in ["http", "https"] -> scheme
_other -> "http"
end
end
end
24 changes: 24 additions & 0 deletions apps/ex_nvr_web/lib/ex_nvr_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ defmodule ExNVRWeb.Router do
import ExNVRWeb.UserAuth

pipeline :browser do
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Phoenix.json_library()

plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
Expand All @@ -14,6 +19,11 @@ defmodule ExNVRWeb.Router do
end

pipeline :api do
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Phoenix.json_library()

plug :accepts, ["json", "jpg", "mp4"]
plug :fetch_session
plug :fetch_current_user
Expand All @@ -23,6 +33,11 @@ defmodule ExNVRWeb.Router do
plug :require_authenticated_user, api: true
end

pipeline :reverse_proxy do
plug ExNVRWeb.Plug.ProxyAllow
plug ExNVRWeb.Plug.ProxyPathRewriter
end

scope "/api", ExNVRWeb do
pipe_through [:api, :require_webhook_token, ExNVRWeb.Plug.Device]

Expand Down Expand Up @@ -150,4 +165,13 @@ defmodule ExNVRWeb.Router do
live "/users/:id", UserLive, :edit
end
end

scope "/service" do
pipe_through [:reverse_proxy]

forward "/", ReverseProxyPlug,
upstream: &ExNVRWeb.ReverseProxy.reverse_proxy/1,
error_callback: &ExNVRWeb.ReverseProxy.handle_error/2,
preserve_host_header: true
end
end
3 changes: 2 additions & 1 deletion apps/ex_nvr_web/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule ExNVRWeb.MixProject do
def project do
[
app: :ex_nvr_web,
version: "0.15.2",
version: "0.16.0",
build_path: "../../_build",
config_path: "../../config/config.exs",
deps_path: "../../deps",
Expand Down Expand Up @@ -55,6 +55,7 @@ defmodule ExNVRWeb.MixProject do
{:logger_json, "~> 5.1"},
{:flop_phoenix, "~> 0.21.1"},
{:prom_ex, "~> 1.9.0"},
{:reverse_proxy_plug, "~> 3.0"},
{:mock, "~> 0.3", only: :test}
]
end
Expand Down
2 changes: 1 addition & 1 deletion apps/ex_nvr_web/priv/static/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ openapi: 3.0.0
info:
title: ExNVR API
description: Manage ExNVR via API endpoints
version: 0.15.2
version: 0.16.0
servers:
- url: '{protocol}://{host}:{port}'
variables:
Expand Down
2 changes: 2 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ config :bundlex, :disable_precompiled_os_deps, apps: [:ex_libsrtp]

config :req, legacy_headers_as_lists: true

config :reverse_proxy_plugin, :http_client, ReverseProxyPlug.HTTPClient.Adapters.HTTPoison

# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{config_env()}.exs"
7 changes: 5 additions & 2 deletions config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ if config_env() == :prod do

config :ex_nvr_web, ExNVRWeb.Endpoint,
http: [
ip: {0, 0, 0, 0},
ip: {0, 0, 0, 0, 0, 0, 0, 0},
port: String.to_integer(System.get_env("EXNVR_HTTP_PORT") || "4000")
],
secret_key_base: secret_key_base,
Expand All @@ -108,7 +108,7 @@ if config_env() == :prod do
if enable_ssl do
config :ex_nvr_web, ExNVRWeb.Endpoint,
https: [
ip: {0, 0, 0, 0},
ip: {0, 0, 0, 0, 0, 0, 0, 0},
port: String.to_integer(System.get_env("EXNVR_HTTPS_PORT") || "443"),
cipher_suite: :compatible,
keyfile: System.get_env("EXNVR_SSL_KEY_PATH"),
Expand All @@ -130,6 +130,9 @@ if config_env() == :prod do
config :logger, level: :info, backends: [LoggerJSON]
end

config :ex_nvr_web,
enable_reverse_proxy: System.get_env("ENABLE_REVERSE_PROXY", "false") == "true"

# ## Configuring the mailer
#
# In production you need to configure the mailer to use a different adapter.
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule ExNVR.Umbrella.MixProject do
use Mix.Project

@version "0.15.2"
@version "0.16.0"

def project do
[
Expand Down
Loading

0 comments on commit 1f6dcec

Please sign in to comment.