├── .exguard.exs ├── .formatter.exs ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── config ├── config.exs ├── dev.exs └── test.exs ├── lib ├── gixir_server.ex └── gixir_server │ ├── application.ex │ ├── git_cli.ex │ ├── ssh.ex │ ├── ssh_command.ex │ ├── ssh_key_authentication.ex │ ├── ssh_session.ex │ └── user.ex ├── mix.exs ├── mix.lock └── test ├── gixir_server └── ssh_session_test.exs ├── gixir_server_test.exs └── test_helper.exs /.exguard.exs: -------------------------------------------------------------------------------- 1 | use ExGuard.Config 2 | 3 | guard("unit-test") 4 | |> command("mix test --color") 5 | |> watch(~r{\.(erl|ex|exs|eex|xrl|yrl)\z}i) 6 | |> ignore(~r{deps}) 7 | |> notification(:off) 8 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | gixir_server-*.tar 24 | 25 | priv/test/ssh_keys 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: elixir 3 | otp_release: 4 | - 20.1 5 | elixir: 6 | - 1.6.0 7 | cache: 8 | directories: 9 | - /home/travis/.mix/ 10 | before_install: 11 | - mkdir -p priv/test/ssh_keys 12 | - ssh-keygen -N '' -b 256 -t ecdsa -f priv/test/ssh_keys/ssh_host_ecdsa_key 13 | - echo -e "Host github.com\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config 14 | script: 15 | - mix test 16 | - MIX_ENV=test mix dialyzer 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Milad Rastian 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GixirServer 2 | 3 | Gixir is a small and extendable Git server currently only working over SSH using [Erlang :ssh](http://erlang.org/doc/man/ssh.html) as SSH server. 4 | 5 | ## Installation 6 | 7 | The package can be installed 8 | by adding `gixir_server` to your list of dependencies in `mix.exs`: 9 | 10 | ```elixir 11 | def deps do 12 | [ 13 | {:gixir_server, github: "slashmili/gixir-server"} 14 | ] 15 | end 16 | ``` 17 | 18 | ## Docs 19 | 20 | - Create required directory: 21 | ```bash 22 | mkdir -p sys_dir 23 | mkdir -p git_home_dir/foo 24 | git init --bare git_home_dir/foo/my-app.git 25 | ``` 26 | 27 | - Create SSH server keys using: 28 | ```bash 29 | ssh-keygen -N '' -b 1024 -t rsa -f sys_dir/ssh_host_rsa_key 30 | ``` 31 | 32 | - Add `:gixir_server` to application list(if running old format mix) 33 | 34 | - Create your Auth module : 35 | ```elixir 36 | defmodule MyApp.UserAuth do 37 | @behaviour GixirServer.User 38 | 39 | def get_user_by_key(_pub_key) do 40 | # look up in your db to find the pub_key 41 | %GixirServer.User{username: "my_user"} 42 | end 43 | 44 | def is_allowed?(%GixirServer.User{} = _current_user, _action, _repository) do 45 | #check if current user can run the action on this repo 46 | true 47 | end 48 | end 49 | ``` 50 | 51 | - Configure your ssh server: 52 | ```elixir 53 | config :gixir_server, GixirServer, 54 | system_dir: "sys_dir/", 55 | port: 2223, 56 | auth_user: MyApp.UserAuth, 57 | git_home_dir: "git_home_dir", 58 | git_bin_dir: "/usr/local/bin/" 59 | ``` 60 | 61 | - Run your Elixir app: 62 | ```elixir 63 | iex -S mix 64 | iex(1)> 65 | ``` 66 | 67 | - Clone your repo: 68 | ```bash 69 | $ git clone ssh://git@localhost:2223/foo/my-app.git 70 | $ cd my-app 71 | $ touch README.md 72 | $ git add README.md 73 | $ git commit -m "hello" 74 | [master (root-commit) e1aa6d0] hello 75 | 1 file changed, 0 insertions(+), 0 deletions(-) 76 | create mode 100644 README.md 77 | $ git push origin HEAD 78 | Counting objects: 3, done. 79 | Writing objects: 100% (3/3), 212 bytes | 212.00 KiB/s, done. 80 | Total 3 (delta 0), reused 0 (delta 0) 81 | To ssh://localhost:2223/foo/my-app.git 82 | * [new branch] HEAD -> master 83 | ``` 84 | 85 | # TODO: 86 | 87 | - bring tests from [gitwerk](https://github.com/carloslima/gitwerk/tree/master/test/git_werk_guts) ssh section to here 88 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :gixir_server, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:gixir_server, :key) 18 | # 19 | # You can also configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :gixir_server, GixirServer, 4 | system_dir: "priv/test/ssh_keys", 5 | port: 0 6 | -------------------------------------------------------------------------------- /lib/gixir_server.ex: -------------------------------------------------------------------------------- 1 | defmodule GixirServer do 2 | @moduledoc """ 3 | SSh Server that handles users authoriztion and git commands on the server 4 | """ 5 | defstruct ssh_pid: nil 6 | 7 | use GenServer 8 | require Logger 9 | 10 | alias __MODULE__ 11 | 12 | def start_link(_) do 13 | GenServer.start_link(__MODULE__, []) 14 | end 15 | 16 | def init(_) do 17 | send(self(), :start_ssh) 18 | {:ok, %__MODULE__{}} 19 | end 20 | 21 | def handle_info(:start_ssh, state) do 22 | ssh_server_opts = Application.get_env(:gixir_server, __MODULE__) 23 | priv_dir = String.to_charlist(ssh_server_opts[:system_dir]) 24 | port = ssh_server_opts[:port] || 0 25 | 26 | {:ok, ssh_pid} = 27 | :ssh.daemon( 28 | port, 29 | system_dir: priv_dir, 30 | user_dir: priv_dir, 31 | key_cb: GixirServer.SshKeyAuthentication, 32 | auth_methods: 'publickey', 33 | shell: &on_shell/2, 34 | ssh_cli: {GixirServer.GitCli, []} 35 | ) 36 | 37 | Process.link(ssh_pid) 38 | 39 | {:noreply, %{state | ssh_pid: ssh_pid}} 40 | end 41 | 42 | def on_shell(username, peer_address) do 43 | Logger.debug( 44 | "new user connected with username #{username} and address #{inspect(peer_address)}" 45 | ) 46 | 47 | spawn_link(fn -> 48 | IO.puts( 49 | "Hi #{username}! You've successfully authenticated, but GixirServer does not provide shell access." 50 | ) 51 | end) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/gixir_server/application.ex: -------------------------------------------------------------------------------- 1 | defmodule GixirServer.Application do 2 | # See https://hexdocs.pm/elixir/Application.html 3 | # for more information on OTP Applications 4 | @moduledoc false 5 | 6 | use Application 7 | 8 | def start(_type, _args) do 9 | # List all child processes to be supervised 10 | children = [ 11 | {Registry, keys: :unique, name: GixirServer.SshSession}, 12 | {GixirServer, []} 13 | # Starts a worker by calling: GixirServer.Worker.start_link(arg) 14 | # {GixirServer.Worker, arg}, 15 | ] 16 | 17 | # See https://hexdocs.pm/elixir/Supervisor.html 18 | # for other strategies and supported options 19 | opts = [strategy: :one_for_one, name: GixirServer.Supervisor] 20 | Supervisor.start_link(children, opts) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/gixir_server/git_cli.ex: -------------------------------------------------------------------------------- 1 | defmodule GixirServer.GitCli do 2 | @moduledoc """ 3 | Implements SSH Channel and provides Git cli to remote connection 4 | """ 5 | @behaviour :ssh_channel 6 | 7 | alias GixirServer.SshSession 8 | require Logger 9 | 10 | @doc false 11 | def init(_) do 12 | {:ok, %{port: nil, channel_id: nil, want_reply: nil, cm: nil}} 13 | end 14 | 15 | @doc false 16 | def handle_msg({p, {:data, data}}, state) when is_port(p) do 17 | :ssh_connection.send(state.cm, state.channel_id, 0, data) 18 | {:ok, state} 19 | end 20 | 21 | def handle_msg({:EXIT, _, _}, state) do 22 | close_ssh_connection(state, :ok) 23 | {:ok, state} 24 | end 25 | 26 | def handle_msg({:ssh_channel_up, channel_id, cm}, state) do 27 | {:ok, %{state | cm: cm, channel_id: channel_id}} 28 | end 29 | 30 | def handle_msg(args, state) do 31 | Logger.debug("GixirServer.GitCli.handle_msg: unmatched: #{inspect(args)}") 32 | {:ok, state} 33 | end 34 | 35 | @doc false 36 | def handle_ssh_msg({:ssh_cm, cm, {:exec, channel_id, want_reply, cmd}}, state) do 37 | port = 38 | with {:ok, session} <- SshSession.get(cm), 39 | {:ok, command} <- GixirServer.Ssh.get_git_command(to_string(cmd), session.user) do 40 | conf = Application.get_env(:gixir_server, GixirServer) 41 | git_dir = conf[:git_bin_dir] || "/usr/local/bin/" 42 | [command, arg01] = String.split(command) 43 | opts = [:binary, :exit_status, {:args, [arg01]}] 44 | git_cmd = "#{git_dir}#{command}" 45 | Port.open({:spawn_executable, git_cmd}, opts) 46 | else 47 | oth -> 48 | :ssh_connection.send(cm, channel_id, 0, "Invalid command: '#{cmd}'\n") 49 | Logger.debug("Failed to exec #{inspect(cmd)} on git server, error: #{inspect(oth)}") 50 | close_ssh_connection(state, :invalid_command) 51 | end 52 | 53 | {:ok, %{state | want_reply: want_reply, port: port}} 54 | end 55 | 56 | def handle_ssh_msg({:ssh_cm, _cm, {:data, _channel_id, _data_type, data}}, state) do 57 | send(state.port, {self(), {:command, data}}) 58 | {:ok, state} 59 | end 60 | 61 | def handle_ssh_msg({:ssh_cm, _cm, {:eof, _channel_id}}, state) do 62 | if Port.info(state.port) == nil do 63 | close_ssh_connection(state, :ok) 64 | end 65 | 66 | {:ok, state} 67 | end 68 | 69 | def handle_ssh_msg({:ssh_cm, _cm, {:shell, _channel_id, _}}, state) do 70 | close_ssh_connection(state, :no_shell) 71 | {:ok, state} 72 | end 73 | 74 | def handle_ssh_msg({:ssh_cm, cm, {:pty, channel_id, _, _}}, state) do 75 | username = 76 | case SshSession.get(cm) do 77 | {:ok, session} -> session.user.username 78 | _ -> "" 79 | end 80 | 81 | message = 82 | "Hi #{username}! You've successfully authenticated, but GitWerk does not provide shell access.\r\n" 83 | 84 | :ssh_connection.send(cm, channel_id, 1, message) 85 | {:ok, state} 86 | end 87 | 88 | def handle_ssh_msg(args, state) do 89 | Logger.debug("GixirServer.GitCli.handle_msg.handle_ssh_msg: unmatched: #{inspect(args)}") 90 | {:ok, state} 91 | end 92 | 93 | defp close_ssh_connection(state, status) do 94 | if status == :ok do 95 | :ssh_connection.exit_status(state.cm, state.channel_id, 0) 96 | else 97 | :ssh_connection.exit_status(state.cm, state.channel_id, 1) 98 | end 99 | 100 | :ssh_connection.close(state.cm, state.channel_id) 101 | end 102 | 103 | def terminate(_reason, _state) do 104 | :ok 105 | end 106 | 107 | @doc false 108 | def handle_cast(args, state) do 109 | Logger.debug("GixirServer.GitCli.handle_cast: unmatched: #{inspect(args)}") 110 | {:noreply, state} 111 | end 112 | 113 | @doc false 114 | def handle_call(args, _, state) do 115 | Logger.debug("GixirServer.GitCli.handle_call: unmatched: #{inspect(args)}") 116 | {:reply, :ok, state} 117 | end 118 | 119 | @doc false 120 | def code_change(args, _, state) do 121 | Logger.debug("GixirServer.GitCli.code_change: unmatched: #{inspect(args)}") 122 | {:reply, :ok, state} 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /lib/gixir_server/ssh.ex: -------------------------------------------------------------------------------- 1 | defmodule GixirServer.Ssh do 2 | alias GixirServer.SshCommand 3 | alias GixirServer.User 4 | 5 | @valid_commands ~w{git-upload-pack git-receive-pack git-upload-archive git-lfs-authenticate} 6 | @commands_to_action %{ 7 | "git-upload-pack" => :clone, 8 | "git-receive-pack" => :push 9 | } 10 | 11 | def valid_command?(""), do: false 12 | 13 | def valid_command?(command) do 14 | exec = 15 | command 16 | |> String.trim() 17 | |> String.split(" ") 18 | |> Enum.at(0) 19 | 20 | Enum.any?(@valid_commands, fn cmd -> 21 | exec != "" and cmd =~ ~r{^#{exec}} 22 | end) 23 | end 24 | 25 | def parse_command(command) do 26 | with true <- valid_command?(command), 27 | %{"command" => cmd, "username" => username, "repository" => repo_name} <- 28 | match_command(command) do 29 | {:ok, %SshCommand{command: cmd, username: username, repository: repo_name}} 30 | else 31 | _ -> :error 32 | end 33 | end 34 | 35 | defp match_command(command) do 36 | Regex.named_captures( 37 | ~r{(?[a-z\-]*) '/(?.*)/(?.*).git'}, 38 | command 39 | ) 40 | end 41 | 42 | def get_git_command(command, current_user) do 43 | with {:ok, cmd} <- parse_command(command), 44 | true <- is_allowed?(cmd, current_user), 45 | {:ok, repo_full_path} <- find_repo_full_path(cmd.username, cmd.repository) do 46 | {:ok, "#{cmd.command} #{repo_full_path}"} 47 | else 48 | {:error, _} = err -> err 49 | reason -> {:error, reason} 50 | end 51 | end 52 | 53 | @doc """ 54 | Checks if user by given key_id is allowed to access 55 | to request repo 56 | """ 57 | def is_allowed?(%SshCommand{} = cmd, %User{} = current_user) do 58 | with action when not is_nil(action) <- Map.get(@commands_to_action, cmd.command), 59 | true <- 60 | User.user_auth_module().is_allowed?( 61 | current_user, 62 | action, 63 | {cmd.username, cmd.repository} 64 | ) do 65 | true 66 | else 67 | _ -> false 68 | end 69 | end 70 | 71 | defp find_repo_full_path(username, repository) do 72 | conf = Application.get_env(:gixir_server, GixirServer) 73 | repo_path = "#{conf[:git_home_dir]}/#{username}/#{repository}.git" 74 | 75 | if File.exists?(repo_path) do 76 | {:ok, "#{conf[:git_home_dir]}/#{username}/#{repository}.git"} 77 | else 78 | {:error, :repo_path_not_found} 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/gixir_server/ssh_command.ex: -------------------------------------------------------------------------------- 1 | defmodule GixirServer.SshCommand do 2 | defstruct command: nil, username: nil, repository: nil 3 | end 4 | -------------------------------------------------------------------------------- /lib/gixir_server/ssh_key_authentication.ex: -------------------------------------------------------------------------------- 1 | defmodule GixirServer.SshKeyAuthentication do 2 | @behaviour :ssh_server_key_api 3 | 4 | require Record 5 | 6 | Record.defrecord( 7 | :RSAPublicKey, 8 | Record.extract(:RSAPublicKey, from_lib: "public_key/include/public_key.hrl") 9 | ) 10 | 11 | Record.defrecord( 12 | :RSAPrivateKey, 13 | Record.extract(:RSAPrivateKey, from_lib: "public_key/include/public_key.hrl") 14 | ) 15 | 16 | Record.defrecord( 17 | :DSAPrivateKey, 18 | Record.extract(:DSAPrivateKey, from_lib: "public_key/include/public_key.hrl") 19 | ) 20 | 21 | Record.defrecord( 22 | :"Dss-Parms", 23 | Record.extract(:"Dss-Parms", from_lib: "public_key/include/public_key.hrl") 24 | ) 25 | 26 | alias GixirServer.SshSession 27 | 28 | @type public_key :: :public_key.public_key() 29 | @type private_key :: map | map | term 30 | @type public_key_algorithm :: :"ssh-rsa" | :"ssh-dss" | atom 31 | @type user :: charlist() 32 | @type daemon_options :: Keyword.t() 33 | 34 | @doc """ 35 | Fetches the private key of the host. 36 | """ 37 | @spec host_key(public_key_algorithm, daemon_options) :: {:ok, private_key} | {:error, any} 38 | def host_key(algorithm, daemon_options) do 39 | with base_name <- file_base_name(algorithm), 40 | file_path <- file_full_path(base_name, daemon_options), 41 | {:ok, bin} <- File.read(file_path), 42 | {:ok, key} <- decode_ssh_file(bin, nil) do 43 | {:ok, key} 44 | else 45 | {:error, :enoent} -> {:error, :no_host_key_found} 46 | {:error, :no_pass_phrase_provided} -> {:error, :failed_to_load_host_key} 47 | _ -> {:error, :no_idea} 48 | end 49 | end 50 | 51 | @doc """ 52 | Checks if the user key is authorized. 53 | """ 54 | @spec is_auth_key(binary, user, daemon_options) :: boolean 55 | def is_auth_key({:RSAPublicKey, _, _} = key, 'git', daemon_options) do 56 | is_auth_key({key, []}, 'git', daemon_options) 57 | end 58 | 59 | def is_auth_key(key, 'git', _daemon_options) do 60 | key_str = 61 | [key] 62 | |> :public_key.ssh_encode(:auth_keys) 63 | |> String.trim() 64 | 65 | case GixirServer.User.user_auth_module().get_user_by_key(key_str) do 66 | nil -> 67 | false 68 | 69 | user -> 70 | update_session(key_str, user) 71 | true 72 | end 73 | end 74 | 75 | def is_auth_key(_, _, _), do: false 76 | 77 | defp get_session do 78 | case SshSession.new() do 79 | {:ok, session} -> 80 | session 81 | 82 | {:error, _} -> 83 | {:ok, session} = SshSession.get() 84 | session 85 | end 86 | end 87 | 88 | defp update_session(key, user) do 89 | session = get_session() 90 | SshSession.update(%{session | public_key: key, user: user}) 91 | end 92 | 93 | defp file_base_name(:"ssh-rsa"), do: "ssh_host_rsa_key" 94 | defp file_base_name(:"rsa-sha2-256"), do: "ssh_host_rsa_key" 95 | defp file_base_name(:"rsa-sha2-384"), do: "ssh_host_rsa_key" 96 | defp file_base_name(:"rsa-sha2-512"), do: "ssh_host_rsa_key" 97 | defp file_base_name(:"ssh-dss"), do: "ssh_host_dsa_key" 98 | defp file_base_name(:"ecdsa-sha2-nistp256"), do: "ssh_host_ecdsa_key" 99 | defp file_base_name(:"ecdsa-sha2-nistp384"), do: "ssh_host_ecdsa_key" 100 | defp file_base_name(:"ecdsa-sha2-nistp521"), do: "ssh_host_ecdsa_key" 101 | defp file_base_name(_), do: "ssh_host_key" 102 | 103 | defp file_full_path(name, opts) do 104 | opts 105 | |> ssh_dir 106 | |> Path.join(name) 107 | end 108 | 109 | defp ssh_dir(opts) do 110 | case Keyword.fetch(opts, :system_dir) do 111 | {:ok, dir} -> dir 112 | :error -> "/etc/ssh" 113 | end 114 | end 115 | 116 | defp decode_ssh_file(pem, password) do 117 | case :public_key.pem_decode(pem) do 118 | [{_, _, :not_encrypted} = entry] -> 119 | {:ok, :public_key.pem_entry_decode(entry)} 120 | 121 | [entry] when password != :ignore -> 122 | {:ok, :public_key.pem_entry_decode(entry, password)} 123 | 124 | _ -> 125 | {:error, :no_pass_phrase_provided} 126 | end 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /lib/gixir_server/ssh_session.ex: -------------------------------------------------------------------------------- 1 | defmodule GixirServer.SshSession do 2 | defstruct public_key: nil, user: nil 3 | 4 | alias __MODULE__ 5 | 6 | @type t :: %SshSession{public_key: binary, user: binary} 7 | 8 | @doc """ 9 | Registers the current process with provided attrs 10 | """ 11 | @spec new(map) :: {:ok, SshSession.t()} | {:error, {:already_registered, pid}} 12 | def new(attrs \\ %{}) do 13 | session = struct(SshSession, attrs) 14 | 15 | with {:ok, _} <- Registry.register(SshSession, self(), session) do 16 | {:ok, session} 17 | end 18 | end 19 | 20 | @doc """ 21 | Updates stored data related to current process 22 | """ 23 | @spec update(t) :: {:ok, SshSession.t()} | {:error, :session_not_found} 24 | def update(%SshSession{} = attrs) do 25 | with {new_value, _old_value} <- Registry.update_value(SshSession, self(), fn _ -> attrs end) do 26 | {:ok, new_value} 27 | else 28 | _ -> {:error, :session_not_found} 29 | end 30 | end 31 | 32 | @doc """ 33 | Gets session for this pid 34 | """ 35 | @spec get(pid | nil) :: {:ok, t} | {:error, :session_not_found} 36 | def get(pid \\ nil) do 37 | pid = pid || self() 38 | 39 | with [{_, value}] <- Registry.lookup(SshSession, pid) do 40 | {:ok, value} 41 | else 42 | _ -> {:error, :session_not_found} 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/gixir_server/user.ex: -------------------------------------------------------------------------------- 1 | defmodule GixirServer.User do 2 | defstruct username: nil, key_id: nil 3 | @type username :: binary 4 | @type key_id :: binary | integer 5 | @type t :: %__MODULE__{username: username, key_id: key_id} 6 | @type action :: :clone | :push 7 | @type repository_owner_name :: binary 8 | @type repository_name :: binary 9 | @type repository_tuple :: {repository_owner_name, repository_name} 10 | @callback get_user_by_key(binary) :: nil | t 11 | @callback is_allowed?(t, action, repository_tuple) :: boolean 12 | 13 | require Logger 14 | 15 | @spec get_user_by_key(binary) :: boolean 16 | def get_user_by_key(_) do 17 | Logger.error( 18 | "You haven't configure :auth_user setttings, please check the docs. Until then all of ssh authentications will fail!" 19 | ) 20 | 21 | nil 22 | end 23 | 24 | def is_allowed?(_, _, _) do 25 | Logger.error( 26 | "You haven't configure :auth_user setttings, please check the docs. Until then all of ssh authentications will fail!" 27 | ) 28 | 29 | false 30 | end 31 | 32 | def user_auth_module do 33 | Application.get_env(:gixir_server, GixirServer)[:auth_user] || __MODULE__ 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule GixirServer.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :gixir_server, 7 | version: "0.1.0", 8 | elixir: "~> 1.6", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [ 17 | mod: {GixirServer.Application, []}, 18 | extra_applications: [:logger, :ssh, :public_key] 19 | ] 20 | end 21 | 22 | # Run "mix help deps" to learn about dependencies. 23 | defp deps do 24 | [ 25 | {:dialyxir, "~> 0.5", only: [:dev, :test], runtime: false}, 26 | {:ex_guard, "~> 1.3", only: :dev}, 27 | {:ex_unit_notifier, "~> 0.1", only: :test}, 28 | {:ex_doc, "~> 0.18.1", only: :dev}, 29 | {:earmark, "~> 1.2", only: :dev} 30 | ] 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [], [], "hexpm"}, 3 | "earmark": {:hex, :earmark, "1.2.4", "99b637c62a4d65a20a9fb674b8cffb8baa771c04605a80c911c4418c69b75439", [], [], "hexpm"}, 4 | "ex_doc": {:hex, :ex_doc, "0.18.3", "f4b0e4a2ec6f333dccf761838a4b253d75e11f714b85ae271c9ae361367897b7", [], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, 5 | "ex_guard": {:hex, :ex_guard, "1.3.0", "0f5c50b90a7e4c599b45d02448ae53eabffc33adb7bfdfc5f5507715e7662a25", [], [{:fs, "~> 0.9", [hex: :fs, repo: "hexpm", optional: false]}], "hexpm"}, 6 | "ex_unit_notifier": {:hex, :ex_unit_notifier, "0.1.4", "36a2dcab829f506e01bf17816590680dd1474407926d43e64c1263e627c364b8", [], [], "hexpm"}, 7 | "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [], [], "hexpm"}, 8 | } 9 | -------------------------------------------------------------------------------- /test/gixir_server/ssh_session_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GixirServer.SshSessionTest do 2 | use ExUnit.Case 3 | alias GixirServer.SshSession 4 | doctest SshSession 5 | 6 | test "creates only one session for current PID with same data" do 7 | assert {:ok, %SshSession{public_key: "my_public", user: "user"}} == 8 | SshSession.new(%{public_key: "my_public", user: "user"}) 9 | 10 | assert {:error, {:already_registered, _}} = 11 | SshSession.new(%{public_key: "my_public", user: "user"}) 12 | end 13 | 14 | test "should be able to create two sessions with same data for two PIDs" do 15 | test_pid = self() 16 | 17 | spawn_link(fn -> 18 | {:ok, _} = SshSession.new(%{public_key: "my_public", user: "user"}) 19 | send(test_pid, :session_created) 20 | end) 21 | 22 | assert {:ok, _} = SshSession.new(%{public_key: "my_public", user: "user"}) 23 | assert_receive :session_created 24 | end 25 | 26 | test "updates a session for current PID" do 27 | assert {:ok, session} = SshSession.new(%{public_key: nil, user: "user"}) 28 | 29 | assert SshSession.update(%{session | public_key: "pp"}) == 30 | {:ok, %GixirServer.SshSession{public_key: "pp", user: "user"}} 31 | end 32 | 33 | test "should fail when session is not initilized for this PID" do 34 | assert {:error, :session_not_found} = SshSession.update(%SshSession{public_key: "pp"}) 35 | end 36 | 37 | test "gets a session" do 38 | assert {:ok, session} = SshSession.new(%{public_key: "public_key", user: "user"}) 39 | 40 | assert {:ok, ^session} = SshSession.get() 41 | end 42 | 43 | test "should fail if there is no session set" do 44 | assert {:error, :session_not_found} = SshSession.get() 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/gixir_server_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GixirServerTest do 2 | use ExUnit.Case 3 | doctest GixirServer 4 | end 5 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------