Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use regexp for harbor endpoints registration #2599

Merged
merged 36 commits into from
Sep 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
e4c9dc2
Export regexp in Lang, add to_string
toots Sep 4, 2022
46cae3c
Wrap it up!
toots Sep 7, 2022
65a1da1
Implement custom response.
toots Sep 11, 2022
7d0fcb4
Don't store/compare functional values.
toots Sep 11, 2022
ace3317
Escape / in regexp.
toots Sep 11, 2022
51ba3a6
Add tests.
toots Sep 11, 2022
8b66833
Make sure last registered endpoint is executed first.
toots Sep 11, 2022
78cd28a
Use unescape description in harbor registration log.
toots Sep 12, 2022
0fdd71d
Implement named regexp capture.
toots Sep 12, 2022
81bfda4
Move string functions to stdlib
toots Sep 12, 2022
dd68953
Doc.
toots Sep 12, 2022
d00aa14
Finish converting types/regexp.
toots Sep 13, 2022
81ef06a
Update doc.
toots Sep 13, 2022
c0b3877
Fix.
toots Sep 13, 2022
63081b1
Fix time predicate parsing.
toots Sep 13, 2022
2e82aac
Fix http test.
toots Sep 13, 2022
12fc907
More http reply logic to stdlib, add low-level core registration func…
toots Sep 14, 2022
0d372db
Doc for core.
toots Sep 14, 2022
5cad2d4
Fix test.
toots Sep 14, 2022
256694b
Remove socket from request.
toots Sep 14, 2022
2735fdc
Fix test.
toots Sep 15, 2022
df4b2f2
Add convenience method to set a single header.
toots Sep 15, 2022
36d484f
Rename core to simple, document, add more response methods.
toots Sep 15, 2022
5a3fc21
Better doc.
toots Sep 15, 2022
f2c108b
Cleanup.
toots Sep 15, 2022
1669f6b
Cleanup
toots Sep 15, 2022
6a3c759
cleanup
toots Sep 15, 2022
f7dbb74
Cleanup
toots Sep 15, 2022
fe6fdb5
Cleanup
toots Sep 15, 2022
d4342c8
Add middlewqre.
toots Sep 16, 2022
fb39adf
Cleanup.
toots Sep 16, 2022
3a4f5af
Cleanup, test
toots Sep 16, 2022
c6b2829
Demeth in to_ref
toots Sep 16, 2022
8afc7e4
CHANGES
toots Sep 16, 2022
a502de9
Add migrating entry.
toots Sep 16, 2022
5615f89
Use response.html.
toots Sep 16, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Changed:
- Added support for a Javascript build an interpreter.
- Removed support for `%define` variables, superseeded by support for actual
variables in encoders.
- Reimplemented `harbor` http handler API to be more flexible. Added a new
node/express-like registration and middleware API (#2599).
- Switched default persistence for cross and fade-related overrides
to follow documented behavior. By default, `"liq_fade_out"`, `"liq_fade_skip"`,
`"liq_fade_in"`, `"liq_cross_duration"` and `"liq_fade_type"` now all reset on
Expand Down
263 changes: 110 additions & 153 deletions doc/content/harbor_http.md
Original file line number Diff line number Diff line change
@@ -1,82 +1,125 @@
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:

The harbor server can be used as a HTTP server. We provide two type of APIs for this:

Simple API
----------

The `harbor.http.register.simple` function provides a simple, easy to use registration API for quick
HTTP response implementation. This function receives a record describing the request and returns
the HTTP response.

For convenience, a HTTP response builder is provided via `harbor.http.response`. Here's an example:

```liquidsoap
harbor.http.register(port=8080,method="GET",uri,handler)
def handler(request) =
log("Got a request on path #{request.path}, protocol version: #{request.http_version}, \
method: #{request.method}, headers: #{request.headers}, query: #{request.query}, \
data: #{request.data}")

harbor.http.response(
content_type="text/html",
data="<p>ok, this works!</p>"
)
end

harbor.http.register.simple(port=8080, method="GET", path, 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.
* `path` is the matched path. It can include named fragments, e.g. `"/users/:id/collabs/:cid"`. Named named framents are passed via `request.query`, for instance: `req.query["cid"]`.

* `handler` is the function used to process requests.
Node/express API
----------------

`handler` function has type:
The `harbor.http.register` function offers a higher-level API for advanced HTTP response implementation.
Its API is very similar to the node/express API. Here's an example:

```
(~protocol:string, ~data:string,
~headers:[(string*string)], string)->'a))->unit
where 'a is either string or ()->string
```
```liquidsoap
def handler(request, response) =
log("Got a request on path #{request.path}, protocol version: #{request.http_version}, \
method: #{request.method}, headers: #{request.headers}, query: #{request.query}, \
data: #{request.data}")

where:
# Set response code. Defaults to 200
response.status_code(201)

# Set response status message. Uses `status_code` if not specified
response.status_message("Created")

# Replaces response headers
response.headers(["X-Foo", "bar"])

# Set a single header
response.header("X-Foo", "bar")

# Set http protocol version
response.http_version("1.1")

# Same as setting the "Content-Type" header
response.content_type("application/liquidsoap")

* `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"`
# Set response data. Can be a `string` or a function of type `()->string` returning an empty string
# when done such as `file.read`
response.data("foo")

# Advanced wrappers:

The `handler` function returns HTTP and HTML data to be sent to the client,
for instance:
# Sets content-type to json and data to `json.stringify({foo = "bla"})`
response.json({foo = "bla"})

# Sets `status_code` and `Location:` header for a HTTP redirect response. Takes an optional `status_code` argument.
response.redirect("http://...")

# Sets content-type to html and data to `"<p>It works!</p>"`
response.html("<p>It works!</p>")
end

```
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>
harbor.http.register(port=8080, method="GET", path, handler)
```

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

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.
* `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"`
* `path` is the matched path. It can include named fragments, e.g. `"/users/:id/collabs/:cid"`. Matched named framents are passed via `request.query`, for instance: `req.query["cid"]`.

For convenience, two functions, `http.response` and `http.response.stream` are provided to
create a HTTP response string. `http.response` has the following type:
The handler function receives a record containing all the information about the request and fills
up the details about the response, which is then used to write a proper HTTP response to the client.

```
(?protocol:string,?code:int,?headers:[(string*string)],
?data:string)->string
```
Named fragments from the request path are passed to the response `query` list.

where:
Middleware _a la_ node/express are also supported and registered via `http.harbor.middleware.register`. See `http.harbor.middleware.cors` for an example.

* `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 `""`)
Advanced usage
--------------

`http.response.stream` has the following type:
All registration functions have a `.regexp` counter part, e.g. `harbor.http.register.simple.regexp`. These function accept
a full regular expression for their `path` argument. Named matches on the regular expression are also passed via the request's `query`
parameter.

```
(?protocol:string,?code:int,?headers:[(string*string)],
data_len:int,data:()->string)->string
It is also possible to directly interact with the underlying socket using the `simple` API:

```liquidsoap
# Custom response
def handler(req) =
req.socket.write("HTTP/1.0 201 YYR\r\nFoo: bar\r\n\r\n")
req.socket.close()

# Null indicates that we're using the socket directly.
null()
end

harbor.http.register.simple("/custom", port=3456, handler)
```

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
Examples
--------

These functions can be used to create your own HTTP interface. Some examples
are:
Expand All @@ -91,59 +134,21 @@ 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:

```liquidsoap
# 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")
def redirect_icecast(request, response) =
response.redirect("http://localhost:8000#{request.path}")
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)
harbor.http.register.regexp(
port=8005,
method="GET",
r/^\/(?!admin)/,
redirect_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
Expand All @@ -154,41 +159,19 @@ using the [JSON export function](json.html) `json.stringify`:
meta = ref([])

# s = some source
s.on_metadata(fun (m) -> meta := m)

# 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)
)
# Return the json content of meta
def get_meta(_, response) =
response.json(!meta)
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`
a GET request for `/getmeta` at port `7000`
returns the following:

```
Expand All @@ -203,9 +186,6 @@ Content-Type: application/json; charset=utf-8
}
```

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
Expand All @@ -218,13 +198,9 @@ updates the metadata of a given source. For instance:
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)

def set_meta(request, response) =
# Filter out unusual metadata
meta = metadata.export(snd(x))
meta = metadata.export(request.query)

# Grab the returned message
ret =
Expand All @@ -235,13 +211,7 @@ def set_meta(~protocol,~data,~headers,uri) =
"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>"
)
response.html("<html><body><b>#{ret}</b></body></html>")
end

# Register handler on port 700
Expand All @@ -251,16 +221,3 @@ 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.


15 changes: 14 additions & 1 deletion doc/content/language.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,22 @@ Regular expression flags are:
Regular expressions have the following methods:

* `replace(fn, s)`: replace matched substrings of `s` using function `fn`. If the `g` flag is not passed, only the first match is replaced otherwise, all matches are replaced
* `exec(s)`: execute the regular expression and return a list matches of the form: `[(<match index>, <match>), ..]`
* `split(s)`: split the given string on all substrings matching the regular expression.
* `test(s)`: returns `true` if the given string matches the regular expression.
* `exec(s)`: execute the regular expression and return a of list matches of the form: `[(<match index>, <match>), ..]`. Named matches are also supported and returned as property `groups` of type `[string * string]`:
```liquidsoap
r/(foo)(?<gno>gni)?/g.exec("foogni")
- : [int * string].{groups : [string * string]} =
[
(2, "gni"),
(1, "foo"),
(0, "foogni")
].{
groups = [
("gno", "gni")
]
}
```

### Booleans

Expand Down
6 changes: 6 additions & 0 deletions doc/content/migrating.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ Liquidsoap.
From 2.1.x to 2.2.x
-------------------

### Harbor HTTP server

The API for registering HTTP server endpoint was completely. It should be more flexible and
provide node/express like API for registering endpoints and middleware. You can checkout [the harbor HTTP documentation](harbor_http.html)
for more details.

### Metadata overrides

Some metadata overrides have been made to reset on track boundaries. Previously, those were permanent even though they
Expand Down
Loading