Harbor as HTTP server

The harbor server can be used as a HTTP server. You can use the function harbor.http.register to register HTTP handlers. Its parameters are are follow:

harbor.http.register(port=8080,method="GET",uri,handler)

where:

  • port is the port where to receive incoming connections

  • method is for the http method (or verb), one of: "GET", "PUT", "POST", "DELETE", "OPTIONS" and "HEAD"

  • uri is used to match requested uri. Perl regular expressions are accepted.

  • handler is the function used to process requests.

handler function has type:

(~protocol:string, ~data:string, 
   ~headers:[(string*string)], string)->'a))->unit
  where 'a is either string or ()->string

where:

  • protocol is the HTTP protocol used by the client. Currently, one of "HTTP/1.0" or "HTTP/1.1"
  • data is the data passed during a POST request
  • headers is the list of HTTP headers sent by the client
  • string is the (unparsed) uri requested by the client, e.g.: "/foo?var=bar"

The handler function returns HTTP and HTML data to be sent to the client, for instance:

HTTP/1.1 200 OK\r\n\
  Content-type: text/html\r\n\
  Content-Length: 35\r\n\
  \r\n\
  <html><body>It works!</body></html>

(\r\n should always be used for line return in HTTP content)

The handler is a string getter, which means that it can be of either type string or type ()->string. The former is used to return the response in one call while the later can be used to returned bigger response without having to load the whole response string in memory, for instance in the case of a file.

For convenience, two functions, http.response and http.response.stream are provided to create a HTTP response string. http.response has the following type:

(?protocol:string,?code:int,?headers:[(string*string)],
   ?data:string)->string

where:

  • protocol is the HTTP protocol of the response (default HTTP/1.1)
  • code is the response code (default 200)
  • headers is the response headers. It defaults to [] but an appropriate "Content-Length" header is added if not set by the user and data is not empty.
  • data is an optional response data (default "")

http.response.stream has the following type:

(?protocol:string,?code:int,?headers:[(string*string)],
   data_len:int,data:()->string)->string

where:

  • protocol is the HTTP protocol of the response (default HTTP/1.1)
  • code is the response code (default 200)
  • headers is the response headers. It defaults to [] but an appropriate "Content-Length" header is added if not set by the user and data is not empty.
  • data_len is the length of the streamed response
  • data is the response stream

These functions can be used to create your own HTTP interface. Some examples are:

Redirect Icecast’s pages

Some source clients using the harbor may also request pages that are served by an icecast server, for instance listeners statistics. In this case, you can register the following handler:

# Redirect all files other
  # than /admin.* to icecast,
  # located at localhost:8000
  def redirect_icecast(~protocol,~data,~headers,uri) =
     http.response(
       protocol=protocol,
       code=301,
       headers=[("Location","http://localhost:8000#{uri}")]
     )
  end
  
  # Register this handler at port 8005
  # (provided harbor sources are also served
  #  from this port).
  harbor.http.register(port=8005,method="GET",
                       "^/(?!admin)",
                       redirect_icecast)

Another alternative, less recommended, is to directly fetch the page’s content from the Icecast server:

# Serve all files other
  # than /admin.* by fetching data
  # from Icecast, located at localhost:8000
  def proxy_icecast(~protocol,~data,~headers,uri) =
    def f(x) =
      # Replace Host
      if string.case(lower=false, fst(x)) == "HOST" then
        "Host: localhost:8000"
      else
        "#{fst(x)}: #{snd(x)}"
      end
    end
    headers = list.map(f,headers)
    headers = string.concat(separator="\r\n",headers)
    request = 
      "#{method} #{uri} #{protocol}\r\n\
       #{headers}\r\n\r\n"
    process.read("echo #{quote(request)} | \
                        nc localhost 8000")
  end
  
  # Register this handler at port 8005
  # (provided harbor sources are also served
  #  from this port).
  harbor.http.register(port=8005,method="GET",
                       "^/(?!admin)",
                       proxy_icecast)

This method is not recommended because some servers may not close the socket after serving a request, causing nc and liquidsoap to hang.

Get metadata

You can use harbor to register HTTP services to fecth/set the metadata of a source. For instance, using the JSON export function json.stringify:

meta = ref([])
  
  # s = some source
  
  # Update current metadata
  # converted in UTF8
  def update_meta(m) =
    m = metadata.export(m)
    recode = string.recode(out_enc="UTF-8")
    def f(x) =
      (recode(fst(x)),recode(snd(x)))
    end
    meta := list.map(f,m)
  end
  
  # Apply update_metadata
  # every time we see a new
  # metadata
  s = on_metadata(update_meta,s)
  
  # Return the json content
  # of meta
  def get_meta(~protocol,~data,~headers,uri) =
    m = !meta
    http.response(
      protocol=protocol,
      code=200,
      headers=[("Content-Type","application/json; charset=utf-8")],
      data=json.stringify(m)
    )
  end
  
  # Register get_meta at port 700
  harbor.http.register(port=7000,method="GET","/getmeta",get_meta)

Once the script is running, a GET/POST request for /getmeta at port 7000 returns the following:

HTTP/1.1 200 OK
  Content-Type: application/json; charset=utf-8

  {
    "genre": "Soul",
    "album": "The Complete Stax-Volt Singles: 1959-1968 (Disc 8)",
    "artist": "Astors",
    "title": "Daddy Didn't Tell Me"
  }

Which can be used with AJAX-based backends to fetch the current metadata of source s

Set metadata

Using insert_metadata, you can register a GET handler that updates the metadata of a given source. For instance:

# s = some source
  
  # Create a source equiped with a `insert_metadata` method:
  s = insert_metadata(s)
  
  # The handler
  def set_meta(~protocol,~data,~headers,uri) =
    # Split uri of the form request?foo=bar&...
    # into (request,[("foo","bar"),..])
    x = url.split(uri)
  
    # Filter out unusual metadata
    meta = metadata.export(snd(x))
    
    # Grab the returned message
    ret =
      if meta != [] then
        s.insert_metadata(meta)
        "OK!"
      else
        "No metadata to add!"
    end
  
    # Return response
    http.response(
     protocol=protocol,
     code=200,
     headers=[("Content-Type","text/html")],
     data="<html><body><b>#{ret}</b></body></html>"
    )
  end
  
  # Register handler on port 700
  harbor.http.register(port=7000,method="GET","/setmeta",set_meta)

Now, a request of the form http://server:7000/setmeta?title=foo will update the metadata of source s with [("title","foo")]. You can use this handler, for instance, in a custom HTML form.

Limitations

When using harbor’s HTTP server, please be warned that the server is not meant to be used under heavy load. Therefore, it should not be exposed to your users/listeners if you expect many of them. In this case, you should use it as a backend/middle-end and have some kind of caching between harbor and the final user. In particular, the harbor server is not meant to server big files because it loads their entire content in memory before sending them. However, the harbor HTTP server is fully equipped to serve any kind of CGI script.