Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- defmodule RMAS.WebServer do
- ## setup
- @moduledoc false
- require Logger
- use Raxx.Server
- use Raxx.Static, "../dist"
- ## variables
- {:ok, contents} = File.read("./dist/index.html")
- @contents contents
- @format [pretty: true, limit: :infinity, width: :infinity]
- @jwt_key "some-really-secret-guid"
- ## handle requests
- @doc "
- - handle OPTIONS for CORS pre-flight
- - handles /sse
- - handle /api
- - fallback for regular content"
- def handle_head(r, s) do
- Logger.info("[#{inspect self()}] #{inspect r, @format}")
- try do
- case r do
- %{method: :TRACE} -> trace_request(r, s)
- %{path: ["api" | _], method: :OPTIONS} -> preflight_request(r, s)
- # %{path: ["api" | _]} -> api_request(r, s)
- # %{path: ["sse" | _]} -> sse_request(r, s)
- # _ -> general_request(r, s)
- _ -> {[], s}
- end
- rescue
- ex ->
- Logger.error("[#{inspect self()}] #{inspect(ex)}\n#{Exception.format_stacktrace(System.stacktrace)}")
- {[response(500) |> set_body(false)], s}
- end
- end
- def handle_info(r, s) do
- Logger.warn "#{inspect(r, @format)}"
- r
- end
- def handle_data(data, {r, buffer, s}) do
- r = %{r | body: buffer <> data}
- case r do
- %{path: ["api" | _]} -> api_request(r, s)
- _ -> general_request(r, s)
- end
- end
- defp trace_request(%{body: body}, state) do
- outbound = [
- response(200)
- |> set_body(body)
- ]
- {outbound, state}
- end
- defp preflight_request(%{headers: headers}, state) do # OPTIONS -> handle pre-filght correctly
- h = Enum.into(headers, %{})
- m = h["access-control-request-method"]
- cors = [
- {"access-control-allow-credentials", "true"},
- {"access-control-allow-headers", "authorization,content-type"},
- {"access-control-allow-origin", h["origin"]}
- ]
- cors =
- if m in ["DELETE", "PUT", "PATCH"] do
- cors ++ [{"access-control-allow-methods", m}]
- else
- cors
- end
- outbound = [
- response(204)
- |> set_headers(cors)
- |> set_body(false)
- ]
- Logger.info("[#{inspect self()}] #{inspect outbound, @format}")
- {outbound, state}
- end
- def api_request(req, state) do # api -> cors, request/response + json, check token
- outbound = [
- proc_request(req)
- ]
- {outbound, state}
- end
- defp sse_request(%{headers: headers}, state) do # sse
- h = Enum.into(headers, %{})
- outbound = [
- response(200)
- |> set_header("content-type", "text/event-stream")
- |> set_header("access-control-allow-origin", h["origin"])
- |> set_header("access-control-allow-credentials", "true")
- |> set_body(true)
- ]
- {outbound, state}
- end
- defp general_request(_req, state) do # fallback -> request/response
- outbound = [
- response(200)
- |> set_header("content-type", "text/html; charset=utf-8")
- |> set_body(@contents)
- ]
- {outbound, state}
- end
- ## Report Runner
- defp proc_request(%{method: :POST, path: ["api", "report-runner"], body: data}) do
- with {:ok, %{userid: userid, code: code, date: date}} <- Antidote.decode(data, keys: :atoms) do
- RMAS.Server.run(userid, code, date, true)
- cors_response(200)
- else
- e ->
- Logger.warn("#{inspect(e, @format)}")
- cors_response(400)
- end
- end
- ## Auth
- defp proc_request(%{method: :POST, path: ["api", "auth", "login"], body: data}) do
- with {:ok, m} <- Antidote.decode(data, keys: :atoms),
- {:ok, 1, _, [u]} <- DB.Users.list(%{email: m.username}),
- {:ok, true} <- verify_password(u.password, m.password) do
- token = generate_token(u.user_id)
- r = %{user: un_struct(u), token: token}
- json = Antidote.encode!(r)
- cors_response(json)
- else
- {:error, msg} ->
- Logger.warn("#{inspect(msg, @format)}")
- cors_response(:unauthorized)
- _ -> cors_response(:unauthorized)
- end
- end
- defp proc_request(%{method: :GET, path: ["api", "auth", "test"]}) do
- cors_response(%{status: :OK})
- end
- ## Other
- defp proc_request(%{method: :POST, path: ["api", "clearlogs"]}), do: run(204, DB.execute("truncate table systemlog"))
- defp proc_request(%{method: :GET, path: ["api", "systemlog"]}), do: run(200, DB.execute("select id, l.user_id, isnull(u.name,'SYSTEM') runner, batch, report, report_dt, status, dt_start, dt_end, DATEDIFF(MS, dt_start, dt_end) as [ms_durn] from systemlog l left join users u on u.user_id = l.user_id order by batch desc, id"))
- defp proc_request(%{method: :GET, path: ["api", entity]}), do: run(200, DB.execute("select * from #{entity}"))
- defp proc_request(%{method: :GET, path: ["api", entity, id]}), do: run(200, DB.select(id, entity))
- ## Users
- defp proc_request(%{method: :GET, path: ["api", "users"]}), do: run(200, DB.Users.list())
- defp proc_request(%{method: :GET, path: ["api", "users", id]}), do: run(200, DB.Users.get(id))
- defp proc_request(%{method: :POST, path: ["api", "users"], body: data}) do
- u = Antidote.decode!(data, keys: :atoms)
- u = %{u | password: hash_password("password"), reset_required: true}
- with {:ok, _, _, u} <- DB.Users.create(u) do
- cors_response(un_struct(u), 201, [{"location", "/api/users/#{u.user_id}"}])
- else
- e ->
- Logger.warn("#{inspect(e, @format)}")
- cors_response(400)
- end
- end
- defp proc_request(%{method: :PUT, path: ["api", "users", id], body: data}) do
- {:ok, 1, _, u} = DB.Users.get(id)
- o = Antidote.decode!(data, keys: :atoms)
- {password, reset} =
- if o.password === "" or o.password === "0000000000" do
- {u.password, o.reset_required}
- else
- {hash_password(o.password), false}
- end
- o = %{o | password: password, reset_required: reset}
- run(200, DB.Users.update(o))
- end
- defp proc_request(%{method: :DELETE, path: ["api", "users", id]}), do: run(204, DB.Users.delete(id))
- ## fallback
- defp proc_request(%{path: ["api"]}), do: cors_response("api-root")
- defp proc_request(%{path: ["api" | _]}), do: cors_response(404)
- ## internal
- ## Security
- def hash_password(password) do
- # create a unique salt and salt string
- salt = :crypto.strong_rand_bytes(16)
- salt_string = Base.encode64(salt)
- # create the hash passing in the password, the salt, the hmac, the number of iterations and the bytes to be returned
- hash_bytes = pbkdf2(password, salt)
- hash = Base.encode64(hash_bytes)
- "#{salt_string}|#{hash}"
- end
- def verify_password(hashed_password, provided_password) do
- # split the hash from the salt, like we stored it before
- [stored_salt, stored_hash] = String.split(hashed_password, "|")
- # get the byte arrays of the strings
- salt_bytes = Base.decode64!(stored_salt)
- # create the hash exactly how we did before, but with the stored salt
- calc_hash_bytes = pbkdf2(provided_password, salt_bytes)
- # string calc_hash = Convert.ToBase64String(calc_hash_bytes);
- calc_hash = Base.encode64(calc_hash_bytes)
- {:ok, calc_hash === stored_hash}
- end
- defp pbkdf2(password, salt), do: pbkdf2(password, salt, :sha256, 20000, 32, 1, [], 0)
- defp pbkdf2(_password, _salt, _digest, _rounds, dklen, _block_index, acc, length) when length >= dklen do
- key = acc |> Enum.reverse |> IO.iodata_to_binary
- <<bin::binary-size(dklen), _::binary>> = key
- bin
- end
- defp pbkdf2(password, salt, digest, rounds, dklen, block_index, acc, length) do
- initial = :crypto.hmac(digest, password, <<salt::binary, block_index::integer-size(32)>>)
- block = iterate(password, digest, rounds - 1, initial, initial)
- pbkdf2(password, salt, digest, rounds, dklen, block_index + 1,
- [block | acc], byte_size(block) + length)
- end
- defp iterate(_password, _digest, 0, _prev, acc), do: acc
- defp iterate(password, digest, round, prev, acc) do
- next = :crypto.hmac(digest, password, prev)
- iterate(password, digest, round - 1, next, :crypto.exor(next, acc))
- end
- defp generate_token(user_id) do
- JsonWebToken.sign(%{user_id: user_id, jti: UUID.uuid4()}, %{key: @jwt_key})
- end
- ## utility
- defp un_struct(l) when is_list(l), do: Enum.map(l, &un_struct/1)
- defp un_struct(s = %_{}), do: Map.from_struct(s)
- defp un_struct(s), do: s
- defp cors_response(code) when is_atom(code), do: cors_response(nil, code)
- defp cors_response(code) when is_integer(code), do: cors_response(nil, code)
- defp cors_response(body, code \\ :ok, headers \\ [])
- defp cors_response(body, code, headers), do: api_response(body, code, headers ++ [{"access-control-allow-origin", "*"}, {"access-control-allow-credentials", "true"}])
- defp api_response(body, code, headers) when is_list(headers) do
- response(code)
- |> set_header("content-type", "application/json; charset=utf-8")
- |> set_headers(headers)
- |> check_body(body)
- end
- defp sse_response(body, headers) do
- response(200)
- |> set_headers(headers ++ [{"access-control-allow-credentials", "true"}, {"content-type", "text/event-stream"}])
- |> set_body(body)
- end
- defp check_body(res, nil), do: res
- defp check_body(res, b) when is_binary(b), do: set_body(res, b)
- defp check_body(res, b), do: set_body(res, Antidote.encode!(b))
- defp run(code, {:ok, _count, _durn, nil}), do: cors_response(code)
- defp run(code, {:ok, _count, _durn, value}), do: cors_response(un_struct(value), code)
- defp run(_code, {:error, msg, _sql}), do: cors_response(%{error_msg: msg},400)
- defp run(_code, {:error, x}), do: cors_response(%{error_msg: "#{inspect x}"},400)
- end
Add Comment
Please, Sign In to add comment