README.md

# mllp

An Elixir library for transporting HL7 messages via MLLP (Minimal Lower Layer Protocol)

## Status

This project is not at v1.0 yet. The API and internals will likely change quite a bit between now and v1.0. Also, be aware of the details of the license (Apache 2.0).

## Usage

### HL7 over MLLP

First, let's start an MLLP.Receiver on port 4090.
```
{:ok, r4090} = MLLP.Receiver.start(port: 4090, dispatcher: MLLP.EchoDispatcher)
```

Next, start an MLLP.Client process and store its PID.
```
{:ok, s1} = MLLP.Client.start_link({127,0,0,1}, 4090)
```

Alternatively, you could start a Client using a DNS name rather than an IP address.
```
{:ok, s1} = MLLP.Client.start_link("localhost", 4090)
```


Now send an HL7 message.
```
MLLP.Client.send(s1, HL7.Examples.wikipedia_sample_hl7() |> HL7.Message.new())
```

You will see log info like...
```

15:56:07.217 [debug] Receiver received data: [<<11, 77, 83, 72, 124, 94, 126, 92, 38, 124, 77, 101, 103, 97, 82, 101, 103, 124, 88, 89, 90, 72, 111, 115, 112, 67, 124, 83, 117, 112, 101, 114, 79, 69, 124, 88, 89, 90, 73, 109, 103, 67, 116, 114, 124, 50, 48, 48, 54, 48, ...>>].
 
15:56:07.217 [info] The EchoDispatcher simply logs and discards messages. Message type: mllp_hl7 Message: MSH|^~\&|MegaReg|XYZHospC|SuperOE|XYZImgCtr|20060529090131-0500||ADT^A01^ADT_A01|01052901|P|DG1|1||786.50^CHEST PAIN, UNSPECIFIED^I9|||A|||FFMD^0010^UAMC^L||67890^GRAINGER^LUCY^X^^^MD^0010^UAMC^L|MED|||||A0||13579^POTTER^SHERMAN^T^^^MD^0010^UAMC^L|||||||||||||||||||||||||||2006052909000^^O|||||||0105I30001^^^99DEF^AN
 
15:56:07.218 [debug] MLLP Envelope performed unnecessary unwrapping of unwrapped message
{:error, :application_reject,
 %MLLP.Ack{
   acknowledgement_code: "AR",
   hl7_ack_message: nil,
   text_message: "A real MLLP message dispatcher was not provided"
 }}
```

The Logger `[debug]` part tells us that the Receiver received the HL7 message. The Logger `[info]` part explains that the EchoDispatcher does not route the message anywhere (just "logs and discards" messages). Finally, the return value (`{:error,  :application_reject, ...}`) is a NACK. The EchoDispatcher will not reply with an `:application_accept`.

Now, we will stop the Receiver.

```
 MLLP.Receiver.stop(4090)
```

### Writing a message dispatcher

MLLP does not ship with a default message dispatcher as the cases can vary significantly from domain to domain. Instead,
MLLP provides you with helper libraries (e.g., [HL7](https://hex.pm/packages/elixir_hl7) and helper functions so you can
easily craft your own message dispatcher to suit your needs.

Now we will set up a receiver with a custom dispatcher. 

```
defmodule DemoDispatcher do
  @behaviour MLLP.Dispatcher

  def dispatch(:mllp_hl7, message, state) when is_binary(message) do
    # Put your message handling logic here

    reply =
      MLLP.Ack.get_ack_for_message(
        message,
        :application_accept
      )
      |> to_string()
      |> MLLP.Envelope.wrap_message()

    {:ok, %{state | reply_buffer: reply}}
  end
end
```

Now we can configure a Receiver to use our newly defined DemoDispatcher.

```
{:ok, r4090} = MLLP.Receiver.start(port: 4090, dispatcher: DemoDispatcher)
```

Next, let's set up a Client to exercise our new Receiver.

```
{:ok, s2} = MLLP.Client.start_link({127,0,0,1}, 4090)
```

Now when you send a message to the Receiver's port, the custom DemoDispatcher will be used. 

```
MLLP.Client.send(s2, HL7.Examples.wikipedia_sample_hl7() |> HL7.Message.new())
```

Notice the DemoDispatcher warning is no longer in the Logger output.

```
15:43:26.605 [debug] MLLP Envelope performed unnecessary unwrapping of unwrapped message
 
15:43:26.606 [debug] Receiver received data: [<<11, 77, 83, 72, 124, 94, 126, 92, 38, 124, 77, 101, 103, 97, 82, 101, 103, 124, 88, 89, 90, 72, 111, 115, 112, 67, 124, 83, 117, 112, 101, 114, 79, 69, 124, 88, 89, 90, 73, 109, 103, 67, 116, 114, 124, 50, 48, 48, 54, 48, ...>>].
{:ok, :application_accept,
 %MLLP.Ack{acknowledgement_code: "AA", hl7_ack_message: nil, text_message: ""}}

```

### Non-HL7 over MLLP

While the MLLP framing protocol is mostly for HL7, some companies also send and receive non-HL7 data over MLLP. If you find yourself needing to integrate with a system that has made this choice the following will be helpful.

```
defmodule ExpandedDemoDispatcher do
  @behaviour MLLP.Dispatcher

  def dispatch(:mllp_hl7, message, state) when is_binary(message) do
    # Your message handling logic here

    reply =
      MLLP.Ack.get_ack_for_message(message, :application_accept)
      |> to_string()
      |> MLLP.Envelope.wrap_message()
    
    {:ok, %{state | reply_buffer: reply}}
  end

  def dispatch(:mllp_unknown, message, state) when is_binary(message) do
    # Your message handling logic here

    reply = "Got the BLOB"

    {:ok, %{state | reply_buffer: reply}}
  end
end
```

Now, to use this expanded custom dispatcher with a Client and Receiver.

Let's start by getting a new Receiver.

```
iex> {:ok, r4091} = MLLP.Receiver.start(port: 4091, dispatcher: ExpandedDemoDispatcher)
{:ok,
 %{
   pid: #PID<0.367.0>,
   port: 4091,
   receiver_id: #Reference<0.144001725.3813670918.119240>
 }}
```

Next, start a Client.
```
iex> {:ok, s3} = MLLP.Client.start_link("localhost", 4091)
{:ok, #PID<0.383.0>}

```

Now let's send and receive non-HL7 data over MLLP
```
iex> MLLP.Client.send(s3, "Hip hip hurray")
15:14:20.531 [debug] Receiver received data: [<<11, 72, 105, 112, 32, 104, 105, 112, 32, 104, 117, 114, 114, 97, 121, 28, 13>>].
{:ok, "Got the BLOB"}
```


## **Telemetry** (Client only currently)

Can be namespaced or changed by passing a replacement for DefaultTelemetry.

The default emits `[:mllp, :client, :status | :sending | :received]` telemetry events.
Emitted measurements contain status, errors, timestamps, etc.
The emitted metadata contains the Client state.

## Using TLS
Support for TLS can be added for MLLP protocol to secure the data transfer between a client and receiver. Follow steps below to start a receiver and client using TLS
#### Create certificates
The first step in TLS configuration is to create a TLS certificates, which can be used by the server to start the listener. To help you with creating self signed certificate, run following script:

`sh tls/tls.sh`

This script creates the following certs:
- root ca
- server certificate signed by root ca
- client certificate signed by root ca
- expired client certificate signed by root ca
### Start Receiver

```
iex> MLLP.Receiver.start(port: 8154, dispatcher: MLLP.EchoDispatcher, transport_opts: %{tls: [cacertfile: "tls/root-ca/ca_certificate.pem", verify: :verify_none, certfile: "tls/server/server_certificate.pem", keyfile: "tls/server/private_key.pem"]})
```

### Start Client
```
iex> {:ok, s3} = MLLP.Client.start_link("localhost", 8154, tls: [verify: :verify_peer, cacertfile: "tls/root-ca/ca_certificate.pem"])
```

### Send a message
```
iex> MLLP.Client.send(s3, HL7.Examples.wikipedia_sample_hl7() |> HL7.Message.new())
```

## Using Client Certificates
MLLP listener can enforce client to provide a valid certificate before establishing a successful connection. Follow steps below to use client cert with a listener

### Start MLLP listener with :verify_peer option
```
iex> MLLP.Receiver.start(port: 8154, dispatcher: MLLP.EchoDispatcher, transport_opts: %{tls: [cacertfile: "tls/root-ca/ca_certificate.pem", verify: :verify_peer, certfile: "tls/server/server_certificate.pem", keyfile: "tls/server/private_key.pem"]})
```

### Start MLLP Client with client cert
```
iex> {:ok, s3} = MLLP.Client.start_link("localhost", 8154, tls: [verify: :verify_peer, cacertfile: "tls/root-ca/ca_certificate.pem", certfile: "tls/client/client_certificate.pem", keyfile: "tls/client/private_key.pem"])
```

### Send a message
```
iex> MLLP.Client.send(s3, HL7.Examples.wikipedia_sample_hl7() |> HL7.Message.new())
```

## Using Client Restrictions
MLLP listener supports two options to restrict incoming client connections to make sure it accepts only trusted clients.

1) IP/DNS restriction - In this mode, we can restrict incoming connections using Client IP/DNS.
2) Client Cert Check - If `verify: :verify_peer` option is enabled on listener, it will enforce client to send a valid client cert and will only allow the connection if certificate returned from the client is valid and trusted.

Here are couple of exmaples of using client restrictions
### Options 1 - Client IP/DNS restrictions
#### Start MLLP listener with allowed_clients options
```
iex> MLLP.Receiver.start(port: 8154, dispatcher: MLLP.EchoDispatcher, allowed_clients: ["localhost"])
```
### Start MLLP Client
```
iex> {:ok, s3} = MLLP.Client.start_link("localhost", 8154)
```
### Send a message
```
iex> MLLP.Client.send(s3, HL7.Examples.wikipedia_sample_hl7() |> HL7.Message.new())
```

***In this example starting a client on another server other than localhost will fail and a warning will be logged on the server***

`[warn]  Failed to verify client {ip, port}, error: :client_ip_not_allowed`

***This example provided is without TLS, We can modify it to use with TLS. Make sure to specify `verify: :verify_none` option in transport_opts on the listener. See [Using TLS](#using-tls): for details on setting up TLS connections.***

### Options 2 - Client Cert Check
#### Start MLLP listener with TLS and allowed_clients options
```
iex> MLLP.Receiver.start(port: 8154, dispatcher: MLLP.EchoDispatcher, allowed_clients: ["client-1"], transport_opts: %{tls: [cacertfile: "tls/root-ca/ca_certificate.pem", verify: :verify_peer, certfile: "tls/server/server_certificate.pem", keyfile: "tls/server/private_key.pem"]})
```
### Start MLLP Client
```
iex> {:ok, s3} = MLLP.Client.start_link("localhost", 8154, tls: [verify: :verify_peer, cacertfile: "tls/root-ca/ca_certificate.pem", certfile: "tls/client/client_certificate.pem", keyfile: "tls/client/private_key.pem"])
```

### Send a message
```
iex> MLLP.Client.send(s3, HL7.Examples.wikipedia_sample_hl7() |> HL7.Message.new())
```
***In the above scenarios we start a client with a valid certificate, but the cert issued is not one of the trusted client by the listener, thus the connection fails and a warning is logged by the listener***

`[warn]  Failed to verify client {ip, port}, error: :fail_to_verify_client_cert`

## License

Elixir-MLLP source code is released under Apache 2 License. Check LICENSE file for more information.