A front-end web server specialized for real-time message exchange
Can I write server-side APIs, back-end services, client-side applications, javascript, wasm, even html, in one language and in one code file, and even in one executable binary and process? 〜a certain geek〜
Yes, you can do it with the Caprese. It is free and flexible. Actually, though, it's thanks to a great Nim and libraries.
Is web3 a web? Are there any web server that can be called web3? 〜a certain tweet〜
Caprese will be the base of that system. It would be a decentralized web server with server-to-server connections that could verify the reliability of contents and applications.
I heard you like Ubuntu, so the following are required to install Nim.
sudo apt install build-essential curl
Install with choosenim.
curl https://nim-lang.org/choosenim/init.sh -sSf | sh
echo 'export PATH='$HOME'/.nimble/bin:$PATH' >> ~/.bashrc
. ~/.bashrc
See Nim for installation details.
you also require the following installation to build the SSL libraries, golang is required to build BoringSSL. The version of golang installed by the Ubuntu package tool might be old, so you might want to download and install the latest version from The Go Programming Language, you can choose either.
sudo apt install automake autoconf libtool cmake pkg-config golang
Do you have git installed?
sudo apt install git
Now let's build the Caprese.
git clone https://github.com/zenywallet/caprese
cd caprese
nimble install --depsOnly
nimble depsAll
nim c -r -d:EXAMPLE1 -d:release --opt:speed --threads:on --mm:orc src/caprese.nim
Open https://localhost:8009/ in your browser. You'll probably get a SSL certificate warning, but make sure it's a local server and proceed.
Install Caprese package.
nimble install https://github.com/zenywallet/caprese
It will take quite a while, so make some coffee. The Caprese body is located ~/.nimble/pkgs/caprese-0.1.0/ when installed. The version number may change, though. If you can't find it, try looking for ~/.nimble/pkgs2.
It may be easier to find the path with the following command.
nimble path caprese
In some directory, create server.nim file with the following code.
import caprese
server(ssl = true, ip = "0.0.0.0", port = 8009):
routes:
get "/":
send("Hello!".addHeader())
send("Not Found".addHeader(Status404))
serverStart()
Build and launch.
nim c -r --threads:on server.nim
Open https://localhost:8009/ in your browser. You'll get a SSL certificate warning, but do something.
- Multi-threaded server processing
- WebSocket support
- TLS/SSL support. BearSSL, OpenSSL, LibreSSL, or BoringSSL can be selected depending on the performance and the future security situation
- SNI support for TLS/SSL. Servers can use multiple certificates of different hostnames with the same IP address
- Support for automatic renewal of Let's Encrypt SSL certificates without application restart
- Web pages are in-memory static files at compile time, dynamic file loading is also available for development
- Reverse proxy for backend and internal services, server load balancing is available
- Messaging functionality to send data from the server to clients individually or in groups
- Dependency-free executable for easy server deployment
- Build on your device from source code, this repository does not include binary modules such as SSL libraries
- Languages - Nim 100.0%
- Linux, recommended Debian or Ubuntu
BSD and Windows will be supported
The closure-compiler is needed to minify javascript. It thoroughly optimizes even somewhat verbose and human understandable javascript generated by Nim into short code.
sudo apt install openjdk-19-jre maven
Maven is used to download the closure-compiler. Caprese automatically downloads and runs the closure-compiler internally. To download manually,
mvn dependency:get -Ddest=./ -Dartifact=com.google.javascript:closure-compiler:LATEST
You can find closure-compiler-vyyyyMMdd.jar in the current path. Copy the file to the src path or ~/.nimble/pkgs/caprese-0.1.0/ of the caprese repository, ~/.nimble/pkgs could be ~/.nimble/pkgs2. If several versions of a closure-compiler are found in the path, the latest version is used.
import caprese
const HelloJs = staticScript:
import jsffi
var console {.importc, nodecl.}: JsObject
var helloStr = "Hello,".cstring
var spaceStr = " ".cstring
var worldStr = "world!".cstring
console.log(helloStr & spaceStr & worldStr)
const HelloMinJs = scriptMinifier(code = HelloJs, extern = "")
const HelloHtml = staticHtmlDocument:
buildHtml(html):
head:
meta(charset="utf-8")
body:
tdiv: text "Hello"
script: verbatim HelloMinJs
echo HelloHtml
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body>
<div>Hello</div>
<script>console.log("Hello, world!");</script>
</body>
</html>
It's amazing. HTML is generated from Nim's code, and the wasteful script is simplified. Nothing could be more wonderful. Karax is used to generate HTML. Now that the HTML has been statically generated, all we have to do is return this as a server response with a header.
Register in extern
the names of some variables and functions that should not be changed by the closure-compiler. If a string is specified for extern
, it will be read directly into --externs
option of the closure-compiler via a file. You can also pass extern
a list of keywords you want to prevent the closure-compiler from replacing strings in seq[string]
. The list of keywords will be converted to a readable format by --externs
and passed to the closure-compiler. In addition to them, when the Nim generates javascript, some keywords that should not be changed are automatically added internally to extern
.
The server configuration is written in the config:
block. The config:
block should be set before the server:
block. Below are the default settings. You only need to set the items you want to change or explain explicitly.
config:
sslLib = BearSSL
debugLog = false
sigTermQuit = true
sigPipeIgnore = true
limitOpenFiles = -1
serverWorkerNum = -1
epollEventsSize = 10
soKeepalive = false
tcpNodelay = true
clientMax = 32000
connectionTimeout = 120
recvBufExpandBreakSize = 131072 * 5
maxFrameSize = 131072 * 5
certsPath = "./certs"
privKeyFile = "privkey.pem"
fullChainFile = "fullchain.pem"
httpVersion = 1.1
serverName = "Caprese"
headerServer = false
headerDate = false
headerContentType = true
errorCloseMode = CloseImmediately
connectionPreferred = ExternalConnection
urlRootSafe = true
postRequestMethod = false
sslRoutesHost = SniAndHeaderHost
- sslLib: None, BearSSL(default), OpenSSL, LibreSSL, BoringSSL
Somewhat surprisingly, Caprese supports 4 different SSL libraries. I would like to keep it a secret that BearSSL is the most extreme, with the smallest binary size and the fastest SSL processing speed. Enjoy the differences.
SSL libraries are built from source code in the deps folder of the repository, so there is no need to install SSL libraries on the OS. Just select the SSL library with this parameter and it will be statically linked.
If SSL is not required, it is recommended set to None. This will enable the experimental implementation of fast dispatch processing based on number of client connections and requests. At this time, it is only available for None. - debugLog: true or false(default). If true, debug messages are output to the console.
- sigTermQuit: true(default) or false. If true, handling SIGTERM at the end of the process. The code in the
onQuit:
block is called before the process is terminated. - sigPipeIgnore: Whether to ignore SIGPIPE. Caprese requires SIGPIPE to be ignored, but can be set to false if duplicated in other libraries.
- limitOpenFiles: [Number of open files], -1(default, automatically set the maximum number of open files)
- serverWorkerNum: [Number of processing threads], -1(default, automatically set the number of CPUs in the system)
- connectionTimeout: [Client connection timeout in seconds], -1(disabled). The time to disconnect is not exact. Disconnection occurs between a specified second and twice the time.
- headerServer: true or false(default), If true, include
Server:
in the response headers. Common benchmarks require this value to be true. In benchmark competition, even a single byte of copying can feel heavy. - headerDate: true or false(default), If true, include
Date:
in the response headers. Common benchmarks require this value to be true. It should not be the essence of benchmarking, but sometimes it is a competition of how to implement the date strings. - headerContentType: true(default) or false, If true, include
Content-Type:
in the response headers. It may be possible in some cases to set false according to benchmark requirements. - errorCloseMode: CloseImmediately(default) or UntilConnectionTimeout. Behavior when disconnecting clients on error.
- connectionPreferred: ExternalConnection(default) or InternalConnection. Optimize server processing depending on whether the clients are connected from external or internal network connections. The situation is different when the clients and server are on separate PCs or on the same PC, therefore, the benchmarks should be evaluated separately. If the clients and server are running on the same PC using virtual technology such as Docker and sharing CPU resources, they should rather be considered internal connections.
server(ip = "0.0.0.0", port = 8089):
routes:
get "/":
send(IndexHtml.addHeader())
serverStart()
get "/home":
send(HomeHtml.addHeader())
get "/about":
send(AboutHtml.addHeader())
get "/home", "/about":
send(MainHtml.addHeader())
get re"/([a-z]+)(\d+)":
send(sanitizeHtml(matches[0] & "|" & matches[1]).addHeader())
Considering efficiency, other methods may be better.
get "/user/:username":
send(sanitizeHtml(username).addHeader())
routes:
get "/":
...
let urlText = sanitizeHtml(reqUrl)
send(fmt"Not Found: {urlText}".addDocType().addHeader(Status404))
server(ssl = true, ip = "0.0.0.0", port = 8009):
routes(host = "website1"):
certificates(path = "./certs/website1"):
privKey: "privkey.pem"
fullChain: "fullchain.pem"
get "/":
send(WebSite1Html.addHeader())
server(ip = "0.0.0.0", port = 8089):
routes(host = "website1"):
get "/":
send(WebSite1Html.addHeader())
serverStart()
The server:
block can have more than one before serverStart()
. The host
value of the routes:
block actually sets your domain name. The path of certificates:
block is not the compile-time path, but the run-time path. If the certificate files are updated while the Caprese is running, the new certificates are automatically loaded.
certificates:
privKey: "./certs/priv/privkey.pem"
fullChain: "./certs/chain/fullchain.pem"
config:
...
privKeyFile = "privkey.pem"
fullChainFile = "fullchain.pem"
server(ssl = true, ip = "0.0.0.0", port = 8009):
routes(host = "website1"):
certificates(path = "./certs/website1")
get "/":
send(WebSite1Html.addHeader())
config:
...
certsPath = "./certs"
privKeyFile = "privkey.pem"
fullChainFile = "fullchain.pem"
server(ssl = true, ip = "0.0.0.0", port = 8009):
routes(host = "website1"):
get "/":
send(WebSite1Html.addHeader())
In the above example, the following two files are loaded.
./certs/website1/privkey.pem
./certs/website1/fullchain.pem
The host
value of the routes:
block is used for the file location.
<certsPath>/<routes host>/{<privKeyFile>,<fullChainFile>}
server(ssl = true, ip = "0.0.0.0", port = 8009):
routes(host = "website1"):
certificates(path = "./certs/website1"):
privKey: "privkey.pem"
fullChain: "fullchain.pem"
get "/":
send(WebSite1Html.addHeader())
routes(host = "website2"):
certificates(path = "./certs/website2"):
privKey: "privkey.pem"
fullChain: "fullchain.pem"
get "/":
send(WebSite2Html.addHeader())
serverStart()
I hope you get the idea... This is SNI. There are multiple routes:
blocks in a server:
block. Like the server:
block, the routes:
block can have multiple blocks.
You can also omit the certificates:
block. The following code works the same as the above code. Just remember to prepare the certificate files.
server(ssl = true, ip = "0.0.0.0", port = 8009):
routes(host = "website1"):
get "/":
send(WebSite1Html.addHeader())
routes(host = "website2"):
get "/":
send(WebSite2Html.addHeader())
serverStart()
The runnable level inside the server:
block is called the server dispatch-level. Inside the block is called from multiple threads, it must not call functions that generate long waits and must return results immediately. If the response cannot be returned immediately, return pending first and then process it in another worker thread. Feel free to use async/await in another thread.
type
PendingData = object
url: string
var reqs: Pendings[PendingData]
reqs.newPending(limit = 100)
worker(num = 2):
reqs.recvLoop(req):
let urlText = sanitizeHtml(req.data.url)
let clientId = req.cid
clientId.send(fmt("worker {urlText}").addHeader())
server(ip = "0.0.0.0", port = 8089):
routes(host = "website1"):
get "/test":
reqs.pending(PendingData(url: reqUrl))
let urlText = sanitizeHtml(reqUrl)
send(fmt"Not Found: {urlText}".addDocType().addHeader(Status404))
serverStart()
The send commands executed by another worker thread invoke a server dispatch-level thread to execute the sending process. The number of threads in the server:
block is the number of serverWorkerNum in the config:
block. The same worker threads are used even in a multi-port configuration with multiple server:
blocks, and the number of threads remains the same.
One of the reasons for creating Caprese is stream encryption. The common method of stream encryption using a reverse proxy server in a separate process seems inefficient. To reduce context switches, it would be better to handle stream encryption in the same thread context as the SSL process, like the server:
block in the Caprese.
The return
can be used in routes:
and request methods. I have not found any advantages other than clarifying the return point, but it may be something useful.
server(ip = "0.0.0.0", port = 8089):
routes:
if not serviceActive:
return "Under Maintenance".addHeader(Status503).send
get "/api":
if apiEnabled:
return "OK".addHeader(Status200).send
return "Bad Request".addHeader(Status400).send
serverStart()
Put before the routes:
block in the server:
block. Um, how do I access it? In such a case, Server Thread Context Object Extension may help you.
server(ip = "0.0.0.0", port = 8089):
var localThreadBuffer = newSeq[byte](1024)
routes:
...
To use WebSocket, add a stream:
block in the routes:
block. When a WebSocket connection is established, the onOpen:
block is called. When a message is received, the onMessage:
block is called. When the connection is closed, the onClose:
block is called.
Although a bit tricky to use, WebSockets and web pages can also use the same url path like /
, /ws
. In that case, the get:
path to the web page should be after the stream:
.
server(ssl = true, ip = "0.0.0.0", port = 8009):
routes(host = "website1"):
certificates(path = "./certs/website1"):
privKey: "privkey.pem"
fullChain: "fullchain.pem"
get "/wstest":
send(WsTestHtml.addHeader())
stream(path = "/ws", protocol = "caprese-0.1"):
onOpen:
# client: Client
echo "onOpen"
onMessage:
# client: Client
# opcode: WebSocketOpCode
# data: ptr UncheckedArray[byte]
# size: int
# content: string
echo "onMessage"
wsSend(content, WebSocketOpcode.Binary)
onClose:
# client: Client
echo "onClose"
get "/ws":
send("WebSocket Protocol: caprese-0.1".addHeader())
stream(path = "/ws", protocol = "caprese-0.1"):
# client: Client
onOpen:
echo "onOpen"
# client: Client
# opcode: WebSocketOpCode
# data: ptr UncheckedArray[byte]
# size: int
# content: string
case opcode
of WebSocketOpcode.Binary, WebSocketOpcode.Text, WebSocketOpcode.Continue:
echo "onMessage"
wsSend(content, WebSocketOpcode.Binary)
of WebSocketOpcode.Ping:
wsSend(content, WebSocketOpcode.Pong)
of WebSocketOpcode.Pong:
debug "pong ", content
SendResult.Success
else: # WebSocketOpcode.Close
echo "onClose"
SendResult.None
routes:
get "/wstest":
...
stream "/ws":
...
Use onProtocol:
block.
routes:
...
stream "/ws":
onProtocol:
let prot = reqProtocol()
if prot == "caprese-0.2":
(true, "caprese-0.2")
elif prot == "caprese-0.1":
(true, "caprese-0.1")
else:
(false, "")
onOpen:
...
import caprese
const IndexHtml = staticHtmlDocument:
buildHtml(html):
head:
meta(charset="utf-8")
title: text "WebSocket"
body:
tdiv(id="main")
script(src="/js/app.js")
const AppJs = staticScript:
import std/jsffi
import caprese/jslib
import caprese/jsstream
var stream: Stream = newStream()
stream.connect(url = "wss://localhost:8009/ws", protocol = "test"):
onOpen: stream.send(strToUint8Array("Hi, WebSocket".cstring))
onMessage: document.getElementById("main").innerText = uint8ArrayToStr(data)
const AppMinJs = scriptMinifier(code = AppJs, extern = @[])
type
PendingData = object
msg: string
var wsReqs: Pendings[PendingData]
wsReqs.newPending(limit = 100)
worker(4):
wsReqs.recvLoop(req):
let msgText = sanitizeHtml(req.data.msg)
let clientId = req.cid
clientId.wsSend(msgText)
server(ssl = true, ip = "127.0.0.1", port = 8009):
routes(host = "localhost"):
stream "/ws":
onMessage: wsReqs.pending(PendingData(msg: content))
get "/": IndexHtml.content("html").response
get "/js/app.js": AppMinJs.content("js").response
serverStart()
If you need the ClientId in other ways, you can also get it with the following function in the server:
blocks.
proc markPending(client: Client): ClientId
- reqUrl: URL requested from client, always starts
/
. Caprese will reject requests that do not begin with/
. This means that when concatenating URL strings in a request, it is guaranteed that there will always be a/
between the strings. You may use Nim's/
to concatenate path strings withreqUrl
, but it is not efficient, so I recommend concatenating with&
. - reqHost: Hostname requested in the header from client. It may be different from the hostname negotiated by SSL. Incorrect hostnames should be rejected. If the
host
ofroutes:
is specified, unmatched hosts will be ignored and will not be processed within thatroutes:
block. You may usereqHost
for custom handling withouthost
ofroutes:
. - reqProtocol: WebSocket protocol requested by the client. See Check multiple protocols of the WebSocket for details.
- reqHeader(HeaderID): Get the specific header parameter of the client request by HeaderID. See Http Headers for details.
- reqClient: Pointer to the
client
object currently being processed in the thread context, the same asclient
. Normally,client
should be used.
Maybe you want to use a method called POST, but it is disabled by default on Caprese. Because I never use it for my services to retain integrity. Wouldn't you be happy with the WebSocket stream?
config:
postRequestMethod = true
server(...):
routes:
...
post "/upload":
# client: Client
# data: ptr UncheckedArray[byte]
# size: int
# content: string
echo "post data=", content
...
If the POST data size is large, please use the onMessage:
block as shown below, but it's not implemented yet.
config:
postRequestMethod = true
server(...):
routes:
...
post "/upload":
onStart:
# client: Client
# contentLength: int
# --- open file
onMessage:
# client: Client
# contentLength: int
# pos: int
# data: ptr UncheckedArray[byte]
# size: int
# content: string
# --- append content to file
onDone:
# client: Client
# contentLength: int
# totalSize: int
# --- close file
onFailed:
# client: Client
# contentLength: int
# totalSize: int
# --- delete file
...
You can use onEnd:
block instead of onDone:
and onFailed:
blocks. If onStart:
is called, onEnd:
will always be called.
onEnd:
# client: Client
# contentLength: int
# totalSize: int
if contentLength == totalSize:
# onDone: block body
else:
# onFailed: block body
Custom parameters can be added to the client
object. When custom pragmas {.clientExt.}
are added to a regular object definitions, all the fields of that object are appended to the client
object. However, fields that have already been defined in the client
object cannot be added.
import caprese
type
CipherContext = object
encryptVector: array[16, byte]
decryptVector: array[16, byte]
key: array[140, uint32]
ClientExt {.clientExt.} = object
cipherCtx: CipherContext
proc cipherInit(ctx: var CipherContext) =
zeroMem(addr ctx, sizeof(CipherContext))
echo "ctx=", ctx
server(ssl = true, ip = "0.0.0.0", port = 8009):
routes:
stream "/":
onOpen:
cipherInit(client.cipherCtx)
get "/":
"""<script>new WebSocket("wss://localhost:8009")</script>""".addHeader.send
"Not Found".addHeader(Status404).send
serverStart()
One more thing... The following function may be used to access the client extension object from workers that only know the client ID. However, the client
objects are usually intended to be accessed from the server:
blocks and are not permanently accessible from the workers. In some cases, resource management such as locking may be necessary.
proc getClient(clientId: ClientId): Client
Custom parameters can be added to the server thread ctx
object by the custom pragma {.serverThreadCtxExt.}
.
import caprese
import caprese/bearssl/bearssl_ssl
import caprese/bearssl/bearssl_hash
config:
postRequestMethod = true
type
ServerThreadCtxExt {.serverThreadCtxExt.} = object
sha256Context: br_sha256_context
sha256Buf: array[br_sha256_SIZE, byte]
server(ip = "0.0.0.0", port = 8089):
# ctx initialization here, if you need...
routes:
post "/":
br_sha256_init(addr ctx.sha256Context)
br_sha256_update(addr ctx.sha256Context, data, size.csize_t)
br_sha256_out(addr ctx.sha256Context, addr ctx.sha256Buf)
ctx.sha256Buf.toHex.addHeader.send
serverStart()
There are various efficient ways to parse http headers, though, Caprese uses the approach of predefining only the headers to be used and reading only those headers that are needed. I could not find any servers implementing this approach, so it may be very novel approach. Compared to a fast header parsing algorithm, this approach had an advantage over it.
Enumerate any header IDs you have determined and target strings of headers to be retrieved in the httpHeader:
block. The httpHeader:
block must be in the config:
block.
config:
sslLib = BearSSL
...
httpHeader:
HeaderHost: "Host"
HeaderUserAgent: "User-Agent"
...
Get the header string by specifying the header ID with the reqHeader()
in the routes:
block.
routes:
let userAgent = reqHeader(HeaderUserAgent)
echo userAgent
The reqHeader()
can only be called within the routes:
block contexts, because the headers only manage the read position of the receive buffer, which may be in the server thread context.
Use public:
block. All files in importPath
are statically imported into the code at compile time. Specify the importPath
as relative path like Nim's import
, however, double quotes are necessary. The getProjectPath()
is added internally to the importPath
.
routes:
public(importPath = "../public")
Inside the public:
block, response()
is used, which sends compressed files if the client allows to receive Brotli or Deflate. It also checks the If-None-Match header and return 304 Not Modified if the file has not been changed.
Custom handling such as changing the base URL.
routes:
public(importPath = "../public"):
let retFile = getFile("/webroot" & reqUrl)
if retFile.err == FileContentSuccess:
return response(retFile.data)
You can also create static content objects from static strings with content()
. The content object has uncompressed, Deflate compressed, Brotli compressed, MIME type, SHA256 hash, and MD5 hash.
const IndexHtml = staticHtmlDocument:
buildHtml(html):
...
const AppJs = staticScript:
...
const AppMinJs = scriptMinifier(code = AppJs, extern = "")
server(ssl = true, ip = "0.0.0.0", port = 8009):
routes(host = "website1"):
get "/":
response(content(IndexHtml, "text/html"))
get "/js/app.js":
response(content(AppMinJs, "application/javascript"))
The second argument of content()
can be an extension such as html or js instead of formal MIME types.
get "/":
response(content(IndexHtml, "html"))
get "/js/app.js":
response(content(AppMinJs, "js"))
The Caprese's reverse proxy is different from a typical reverse proxy server and is more simplified. It may be faster than a typical reverse proxy server due to the following specifications. It would be more useful in simple configurations.
- The request URL and http headers are not changed. Since data is sent and received without changing the data to the proxy destination, it would work fine with WebSockets and such.
- When a client makes a request to a proxy path, all subsequent communication is connected to the proxy destination until disconnected. The proxy path is simply compared to the URL, and if the first string matches, proxy forwarding starts. It may be better to add a
/
at the end of the proxy path to make it strict. - External connections are made with SSL, but no SSL inside the proxy. It could be used for internal connections or to connect to back-end services.
import caprese
server(ssl = true, ip = "0.0.0.0", port = 8009):
routes(host = "localhost"):
proxy(path = "/", host = "localhost", port = 8089)
server(ip = "127.0.0.1", port = 8089):
routes(host = "localhost:8009"):
get "/":
response(content("Hello!", "text/html"))
send("Not Found".addHeader(Status404))
serverStart()
It might be better not to check the hostname and port.
server(ip = "127.0.0.1", port = 8089):
routes: # no hostname and port check
get "/":
server(ssl = true, ip = "0.0.0.0", port = 8009):
routes(host = "localhost"):
proxy(path = "/", host = "localhost", port = 8089):
debug "url=", reqUrl
# custom handling here
You can also use UNIX domain sockets. It will provide a fast internal connection.
import caprese
server(ssl = true, ip = "0.0.0.0", port = 8009):
routes(host = "localhost"):
proxy(path = "/", unix = "/tmp/caprese1.sock")
server(unix = "/tmp/caprese1.sock"):
routes(host = "localhost:8009"):
get "/":
response(content("Hello!", "text/html"))
send("Not Found".addHeader(Status404))
serverStart()
It would be better to assign the servers according to the load of each server, but if you simply assign them in order, it would look something like this. There are many ways to do this, and the various configurations will be flexible.
import std/atomics
import caprese
const ProxyHostList = ["host1", "host2", "host3", "host4"]
var proxyHostCounter: Atomic[uint]
proc getProxyHost(): string =
var curIdx = proxyHostCounter.fetchAdd(1) mod ProxyHostList.len.uint
result = ProxyHostList[curIdx]
server(ssl = true, ip = "0.0.0.0", port = 8009):
routes(host = "localhost"):
proxy(path = "/", host = getProxyHost(), port = 8089)
serverStart()
Note: Since proxy:
block is Nim's template, getProxyHost()
function is called after the proxy:
path
is matched.
Let me explain one of the unique features of the Caprese that is not implemented in common web servers. Tags can be attached to client connections. It is possible to send some data to the tag. That data will be sent to all tagged clients. The tag value must be a number or at least 8 bytes of data. It could be a string or something else, but it is better to use hashed data. It is assumed that the data is hashed originally, and no internal hashing of tags is performed. Hashing would be easy with Nim's converter
. To control the tags, you need the ClientId, which you can get with markPending()
.
proc markPending(client: Client): ClientId
proc unmarkPending(clientId: ClientId)
proc unmarkPending(client: Client)
proc setTag(clientId: ClientId, tag: Tag)
proc delTag(clientId: ClientId, tag: Tag)
proc delTags(clientId: ClientId)
proc delTags(tag: Tag)
proc getClientIds(tag: Tag): Array[ClientId]
proc getTags(clientId: ClientId): Array[Tag]
iterator getClientIds(tag: Tag): ClientId
iterator getTags(clientId: ClientId): Tag
template wsSend(tag: Tag, data: seq[byte] | string | Array[byte],
opcode: WebSocketOpCode = WebSocketOpCode.Binary): int
This is a feature that was originally used in the server of the block explorer. What this is used for is that if the HASH160 of addresses in the user wallets are registered as tags, when a new block is found, in the process of parsing the block and transactions, address-related information can be sent to the tags of the found addresses, and the user wallets will be notified in real time.
Non-root users cannot use privileged ports such as 80 or 443 by default, so capabilities must be added on the target server after each build.
sudo setcap cap_net_bind_service=+ep <executable_file_name>
If the above command is not executed, the following bind error will occur.
error: bind ret=-1 errno=13
Check if the capabilities is attached.
getcap <executable_file_name>
At least http port 80 needs to be open for ACME HTTP-01 challenge. One method is to reply ACME tokens on http port 80, another is to redirect http port 80 to https port 443 and reply ACME tokens on the https connection. ACME does not verify certificates, Caprese has self-certificates and can connect with SSL by simply enabling SSL, so SSL connections for ACME are available on https even without certificate files yet. Try redirecting http to https and handling ACME.
import caprese
server(ssl = true, ip = "0.0.0.0", port = 443):
routes(host = "YOUR_DOMAIN"):
certificates(path = "./certs/YOUR_DOMAIN"):
privKey: "privkey.pem"
fullChain: "fullchain.pem"
get "/":
send("Hello!".addHeader())
acme(path = "./www/YOUR_DOMAIN"):
echo "acme ", reqUrl, " ", mime
echo content
send("Not Found".addHeader(Status404))
server(ip = "0.0.0.0", port = 80):
routes(host = "YOUR_DOMAIN"):
send(redirect301("https://YOUR_DOMAIN" & reqUrl))
serverStart()
Create a server.nim file with the above code and launch server as a non-root user. Open ports 80 and 443 to allow connections from external clients.
nim c -d:release --opt:speed --threads:on --mm:orc server.nim
sudo setcap cap_net_bind_service=+ep ./server
./server
With server running, execute the following certbot command as root user. Specify the web root folder where certbot will place the ACME HTTP-01 challenge token.
certbot certonly --webroot -w /path/to/www/YOUR_DOMAIN -d YOUR_DOMAIN
ECDSA or something if you like.
certbot certonly --key-type ecdsa --elliptic-curve secp384r1 --webroot -w /path/to/www/YOUR_DOMAIN -d YOUR_DOMAIN
Or write it in /etc/letsencrypt/cli.ini file.
key-type = ecdsa
elliptic-curve = secp384r1
If successful, the certificate files will be created in the following path.
/etc/letsencrypt/live/YOUR_DOMAIN/{privkey.pem,fullchain.pem}
These files should be copied to the certs folder. Caprese monitors the files in the certs folder and automatically loads the new certificates if any files are changed. However, it is necessary to change the permissions on the certificate files so that user running Caprese can access it.
Create caprese_certs_update.sh, in the following, user and group is assumed to be caprese.
#!/bin/bash
mkdir -p /path/to/certs/YOUR_DOMAIN
cp /etc/letsencrypt/live/YOUR_DOMAIN/{privkey.pem,fullchain.pem} /path/to/certs/YOUR_DOMAIN
chown -R caprese:caprese /path/to/certs
Copy caprese_certs_update.sh to letsencrypt post hook.
cp caprese_certs_update.sh /etc/letsencrypt/renewal-hooks/post
First copy of certificate files, also for testing.
certbot renew --dry-run
Now open http://YOUR_DOMAIN/ in your browser. If the URL http redirects to https and there is no certificate warning, it is successful.
If you have just created the directory /path/to/certs/YOUR_DOMAIN, wait about 30 seconds before opening the URL. This is because if the directory does not exist yet, real-time file monitoring to update the certificates is deactivated. Once the Caprese has detected the directory, monitoring is activated, the certificates will be updated instantly after the certificate files have been changed.
- POST
- IPv6
- QUIC
- Cookies
MIT