├── .formatter.exs ├── .gitignore ├── README.md ├── config └── config.exs ├── lib ├── current_time.ex ├── http.ex └── http │ ├── application.ex │ └── plug_adapter.ex ├── mix.exs ├── mix.lock └── test ├── http_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.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 third-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 | http-*.tar 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Http 2 | 3 | A minimal Elixir HTTP server implementation running [Plug.Octopus](https://github.com/jeffkreeftmeijer/plug_octopus), for [Serving Plug: Building an Elixir HTTP server from scratch](http://blog.appsignal.com/2019/01/22/serving-plug-building-an-elixir-http-server.html). 4 | -------------------------------------------------------------------------------- /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 | # third-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :http, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:http, :key) 18 | # 19 | # You can also configure a third-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 | -------------------------------------------------------------------------------- /lib/current_time.ex: -------------------------------------------------------------------------------- 1 | defmodule CurrentTime do 2 | import Plug.Conn 3 | 4 | def init(options), do: options 5 | 6 | def call(conn, _opts) do 7 | conn 8 | |> put_resp_content_type("text/html") 9 | |> send_resp(200, "Hello world! The time is #{Time.to_string(Time.utc_now())}") 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/http.ex: -------------------------------------------------------------------------------- 1 | defmodule Http do 2 | require Logger 3 | 4 | def start_link(port: port, dispatch: dispatch) do 5 | {:ok, socket} = :gen_tcp.listen(port, active: false, packet: :http_bin, reuseaddr: true) 6 | Logger.info("Accepting connections on port #{port}") 7 | 8 | {:ok, spawn_link(Http, :accept, [socket, dispatch])} 9 | end 10 | 11 | def accept(socket, dispatch) do 12 | {:ok, request} = :gen_tcp.accept(socket) 13 | 14 | spawn(fn -> 15 | dispatch.(request) 16 | end) 17 | 18 | accept(socket, dispatch) 19 | end 20 | 21 | def read_request(request, acc \\ %{headers: []}) do 22 | case :gen_tcp.recv(request, 0) do 23 | {:ok, {:http_request, :GET, {:abs_path, full_path}, _}} -> 24 | read_request(request, Map.put(acc, :full_path, full_path)) 25 | 26 | {:ok, :http_eoh} -> 27 | acc 28 | 29 | {:ok, {:http_header, _, key, _, value}} -> 30 | read_request( 31 | request, 32 | Map.put(acc, :headers, [{String.downcase(to_string(key)), value} | acc.headers]) 33 | ) 34 | 35 | {:ok, line} -> 36 | read_request(request, acc) 37 | end 38 | end 39 | 40 | def send_response(socket, response) do 41 | :gen_tcp.send(socket, response) 42 | :gen_tcp.close(socket) 43 | end 44 | 45 | def child_spec(opts) do 46 | %{id: Http, start: {Http, :start_link, [opts]}} 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/http/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Http.Application do 2 | @moduledoc false 3 | use Application 4 | 5 | def start(_type, _args) do 6 | children = [ 7 | {Http.PlugAdapter, plug: Plug.Octopus, port: 8080} 8 | ] 9 | 10 | opts = [strategy: :one_for_one, name: Http.Supervisor] 11 | Supervisor.start_link(children, opts) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/http/plug_adapter.ex: -------------------------------------------------------------------------------- 1 | defmodule Http.PlugAdapter do 2 | def dispatch(request, plug) do 3 | %{headers: headers, full_path: full_path} = Http.read_request(request) |> IO.inspect() 4 | 5 | %Plug.Conn{ 6 | adapter: {Http.PlugAdapter, request}, 7 | owner: self(), 8 | path_info: path_info(full_path), 9 | query_string: query_string(full_path), 10 | req_headers: headers 11 | } 12 | |> plug.call([]) 13 | end 14 | 15 | def send_resp(socket, status, headers, body) do 16 | response = "HTTP/1.1 #{status}\r\n#{headers(headers)}\r\n#{body}" 17 | 18 | Http.send_response(socket, response) 19 | {:ok, nil, socket} 20 | end 21 | 22 | def child_spec(plug: plug, port: port) do 23 | Http.child_spec(port: port, dispatch: &dispatch(&1, plug)) 24 | end 25 | 26 | defp headers(headers) do 27 | Enum.reduce(headers, "", fn {key, value}, acc -> 28 | acc <> key <> ": " <> value <> "\n\r" 29 | end) 30 | end 31 | 32 | defp path_info(full_path) do 33 | [path | _] = String.split(full_path, "?") 34 | path |> String.split("/") |> Enum.reject(&(&1 == "")) 35 | end 36 | 37 | defp query_string([_]), do: "" 38 | defp query_string([_, query_string]), do: query_string 39 | 40 | defp query_string(full_path) do 41 | full_path 42 | |> String.split("?") 43 | |> query_string 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Http.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :http, 7 | version: "0.1.0", 8 | elixir: "~> 1.9-dev", 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 | applications: [], 18 | extra_applications: [:logger], 19 | mod: {Http.Application, []} 20 | ] 21 | end 22 | 23 | # Run "mix help deps" to learn about dependencies. 24 | defp deps do 25 | [ 26 | {:plug_octopus, github: "jeffkreeftmeijer/plug_octopus"}, 27 | {:plug, "~> 1.7"} 28 | ] 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "cowboy": {:hex, :cowboy, "2.6.1", "f2e06f757c337b3b311f9437e6e072b678fcd71545a7b2865bdaa154d078593f", [:rebar3], [{:cowlib, "~> 2.7.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, 3 | "cowlib": {:hex, :cowlib, "2.7.0", "3ef16e77562f9855a2605900cedb15c1462d76fb1be6a32fc3ae91973ee543d2", [:rebar3], [], "hexpm"}, 4 | "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"}, 5 | "plug": {:hex, :plug, "1.7.1", "8516d565fb84a6a8b2ca722e74e2cd25ca0fc9d64f364ec9dbec09d33eb78ccd", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}], "hexpm"}, 6 | "plug_cowboy": {:hex, :plug_cowboy, "2.0.1", "d798f8ee5acc86b7d42dbe4450b8b0dadf665ce588236eb0a751a132417a980e", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, 7 | "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"}, 8 | "plug_octopus": {:git, "https://github.com/jeffkreeftmeijer/plug_octopus.git", "90a2ecedd3c0e42e0f309d3447c4ef992887ce99", []}, 9 | "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"}, 10 | } 11 | -------------------------------------------------------------------------------- /test/http_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HttpTest do 2 | use ExUnit.Case 3 | doctest Http 4 | 5 | test "greets the world" do 6 | assert Http.hello() == :world 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------