Skip to content

Commit d092dce

Browse files
committed
Nextcloud 32: HaRP support
Signed-off-by: Oleksander Piskun <[email protected]>
1 parent ea7c17e commit d092dce

File tree

7 files changed

+245
-7
lines changed

7 files changed

+245
-7
lines changed

.gitattributes

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Declare files that always have LF line endings on checkout
2+
* text eol=lf

Dockerfile

+22-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
FROM python:3.11-slim-bookworm
22

3+
RUN apt-get update && apt-get install -y curl procps && \
4+
apt-get clean && \
5+
rm -rf /var/lib/apt/lists/*
6+
37
COPY requirements.txt /
48

59
RUN \
@@ -12,7 +16,24 @@ ADD /ex_app/l10[n] /ex_app/l10n
1216
ADD /ex_app/li[b] /ex_app/lib
1317

1418
COPY --chmod=775 healthcheck.sh /
19+
COPY --chmod=775 start.sh /
20+
21+
# Download and install FRP client
22+
RUN set -ex; \
23+
ARCH=$(uname -m); \
24+
if [ "$ARCH" = "aarch64" ]; then \
25+
FRP_URL="https://raw.githubusercontent.com/cloud-py-api/HaRP/main/exapps_dev/frp_0.61.1_linux_arm64.tar.gz"; \
26+
else \
27+
FRP_URL="https://raw.githubusercontent.com/cloud-py-api/HaRP/main/exapps_dev/frp_0.61.1_linux_amd64.tar.gz"; \
28+
fi; \
29+
echo "Downloading FRP client from $FRP_URL"; \
30+
curl -L "$FRP_URL" -o /tmp/frp.tar.gz; \
31+
tar -C /tmp -xzf /tmp/frp.tar.gz; \
32+
mv /tmp/frp_0.61.1_linux_* /tmp/frp; \
33+
cp /tmp/frp/frpc /usr/local/bin/frpc; \
34+
chmod +x /usr/local/bin/frpc; \
35+
rm -rf /tmp/frp /tmp/frp.tar.gz
1536

1637
WORKDIR /ex_app/lib
17-
ENTRYPOINT ["python3", "main.py"]
38+
ENTRYPOINT ["/start.sh"]
1839
HEALTHCHECK --interval=2s --timeout=2s --retries=300 CMD /healthcheck.sh

appinfo/info.xml

+29-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<description>
77
<![CDATA[Simplest skeleton of the Nextcloud application written in python]]>
88
</description>
9-
<version>2.0.0</version>
9+
<version>3.0.0</version>
1010
<licence>MIT</licence>
1111
<author mail="[email protected]" homepage="https://github.com/bigcat88">Alexander Piskun</author>
1212
<namespace>PyAppV2_skeleton</namespace>
@@ -15,7 +15,7 @@
1515
<bugs>https://github.com/nextcloud/app-skeleton-python/issues</bugs>
1616
<repository type="git">https://github.com/nextcloud/app-skeleton-python</repository>
1717
<dependencies>
18-
<nextcloud min-version="29" max-version="31"/>
18+
<nextcloud min-version="29" max-version="32"/>
1919
</dependencies>
2020
<external-app>
2121
<docker-install>
@@ -36,5 +36,32 @@
3636
<description>Test environment without default value</description>
3737
</variable>
3838
</environment-variables>
39+
<routes>
40+
<route>
41+
<url>^/public$</url>
42+
<verb>GET</verb>
43+
<access_level>PUBLIC</access_level>
44+
</route>
45+
<route>
46+
<url>^/user$</url>
47+
<verb>GET</verb>
48+
<access_level>USER</access_level>
49+
</route>
50+
<route>
51+
<url>^/admin$</url>
52+
<verb>GET</verb>
53+
<access_level>ADMIN</access_level>
54+
</route>
55+
<route>
56+
<url>^/$</url>
57+
<verb>GET</verb>
58+
<access_level>USER</access_level>
59+
</route>
60+
<route>
61+
<url>^/ws$</url>
62+
<verb>GET</verb>
63+
<access_level>USER</access_level>
64+
</route>
65+
</routes>
3966
</external-app>
4067
</info>

ex_app/lib/main.py

+109-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
"""Simplest example."""
22

3+
import asyncio
4+
import datetime
35
import os
46
from contextlib import asynccontextmanager
57
from pathlib import Path
8+
from typing import Annotated
69

7-
from fastapi import FastAPI
10+
from fastapi import Depends, FastAPI, Request, WebSocket
11+
from fastapi.responses import HTMLResponse
812
from nc_py_api import NextcloudApp
9-
from nc_py_api.ex_app import AppAPIAuthMiddleware, LogLvl, run_app, set_handlers
13+
from nc_py_api.ex_app import AppAPIAuthMiddleware, LogLvl, nc_app, run_app, set_handlers
1014

1115

1216
@asynccontextmanager
@@ -18,11 +22,113 @@ async def lifespan(app: FastAPI):
1822
APP = FastAPI(lifespan=lifespan)
1923
APP.add_middleware(AppAPIAuthMiddleware) # set global AppAPI authentication middleware
2024

25+
# Build the WebSocket URL dynamically using the NextcloudApp configuration.
26+
WS_URL = NextcloudApp().app_cfg.endpoint + "/exapps/app-skeleton-python/ws"
27+
28+
# HTML content served at the root URL.
29+
# This page opens a WebSocket connection, displays incoming messages,
30+
# and allows you to send messages back to the server.
31+
HTML = f"""
32+
<!DOCTYPE html>
33+
<html>
34+
<head>
35+
<title>FastAPI WebSocket Demo</title>
36+
</head>
37+
<body>
38+
<h1>FastAPI WebSocket Demo</h1>
39+
<p>Type a message and click "Send", or simply watch the server send cool updates!</p>
40+
<input type="text" id="messageText" placeholder="Enter message here...">
41+
<button onclick="sendMessage()">Send</button>
42+
<ul id="messages"></ul>
43+
<script>
44+
// Create a WebSocket connection using the dynamic URL.
45+
var ws = new WebSocket("{WS_URL}");
46+
47+
// When a message is received from the server, add it to the list.
48+
ws.onmessage = function(event) {{
49+
var messages = document.getElementById('messages');
50+
var message = document.createElement('li');
51+
message.textContent = event.data;
52+
messages.appendChild(message);
53+
}};
54+
55+
// Function to send a message to the server.
56+
function sendMessage() {{
57+
var input = document.getElementById("messageText");
58+
ws.send(input.value);
59+
input.value = '';
60+
}}
61+
</script>
62+
</body>
63+
</html>
64+
"""
65+
66+
67+
@APP.get("/")
68+
async def get():
69+
# WebSockets works only in Nextcloud 32 when `HaRP` is used instead of `DSP`
70+
return HTMLResponse(HTML)
71+
72+
73+
@APP.get("/public")
74+
async def public_get(request: Request):
75+
print(f"public_get: {request.headers}", flush=True)
76+
return "Public page!"
77+
78+
79+
@APP.get("/user")
80+
async def user_get(request: Request):
81+
print(f"user_get: {request.headers}", flush=True)
82+
return "Page for the registered users only!"
83+
84+
85+
@APP.get("/admin")
86+
async def admin_get(request: Request):
87+
print(f"admin_get: {request.headers}", flush=True)
88+
return "Admin page!"
89+
90+
91+
@APP.websocket("/ws")
92+
async def websocket_endpoint(
93+
websocket: WebSocket,
94+
nc: Annotated[NextcloudApp, Depends(nc_app)],
95+
):
96+
# WebSockets works only in Nextcloud 32 when `HaRP` is used instead of `DSP`
97+
print(nc.user) # if you need user_id that initiated WebSocket connection
98+
print(f"websocket_endpoint: {websocket.headers}", flush=True)
99+
await websocket.accept()
100+
101+
# This background task sends a periodic message (the current time) every 2 seconds.
102+
async def send_periodic_messages():
103+
while True:
104+
try:
105+
message = f"Server time: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
106+
await websocket.send_text(message)
107+
await asyncio.sleep(2)
108+
except Exception as exc:
109+
NextcloudApp().log(LogLvl.ERROR, str(exc))
110+
break
111+
112+
# Start the periodic sender in the background.
113+
periodic_task = asyncio.create_task(send_periodic_messages())
114+
115+
try:
116+
# Continuously listen for messages from the client.
117+
while True:
118+
data = await websocket.receive_text()
119+
# Echo the received message back to the client.
120+
await websocket.send_text(f"Echo: {data}")
121+
except Exception as e:
122+
NextcloudApp().log(LogLvl.ERROR, str(e))
123+
finally:
124+
# Cancel the periodic message task when the connection is closed.
125+
periodic_task.cancel()
126+
21127

22128
def enabled_handler(enabled: bool, nc: NextcloudApp) -> str:
23129
# This will be called each time application is `enabled` or `disabled`
24130
# NOTE: `user` is unavailable on this step, so all NC API calls that require it will fail as unauthorized.
25-
print(f"enabled={enabled}")
131+
print(f"enabled={enabled}", flush=True)
26132
if enabled:
27133
nc.log(LogLvl.WARNING, f"Hello from {nc.app_cfg.app_name} :)")
28134
else:

healthcheck.sh

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11
#!/bin/bash
22

3-
exit 0
3+
if [ -f /frpc.toml ] && [ -n "$HP_SHARED_KEY" ]; then
4+
if pgrep -x "frpc" > /dev/null; then
5+
exit 0
6+
else
7+
exit 1
8+
fi
9+
fi

start.sh

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#!/bin/bash
2+
set -e
3+
4+
# Check if the configuration file already exists
5+
if [ -f /frpc.toml ]; then
6+
echo "/frpc.toml already exists, skipping creation."
7+
else
8+
# Only create a config file if HP_SHARED_KEY is set.
9+
if [ -n "$HP_SHARED_KEY" ]; then
10+
echo "HP_SHARED_KEY is set, creating /frpc.toml configuration file..."
11+
if [ -d "/certs/frp" ]; then
12+
echo "Found /certs/frp directory. Creating configuration with TLS certificates."
13+
cat <<EOF > /frpc.toml
14+
serverAddr = "$HP_FRP_ADDRESS"
15+
serverPort = $HP_FRP_PORT
16+
metadatas.token = "$HP_SHARED_KEY"
17+
transport.tls.certFile = "/certs/frp/client.crt"
18+
transport.tls.keyFile = "/certs/frp/client.key"
19+
transport.tls.trustedCaFile = "/certs/frp/ca.crt"
20+
21+
[[proxies]]
22+
name = "$APP_ID"
23+
type = "tcp"
24+
localIP = "127.0.0.1"
25+
localPort = $APP_PORT
26+
remotePort = $APP_PORT
27+
EOF
28+
else
29+
echo "Directory /certs/frp not found. Creating configuration without TLS certificates."
30+
cat <<EOF > /frpc.toml
31+
serverAddr = "$HP_FRP_ADDRESS"
32+
serverPort = $HP_FRP_PORT
33+
metadatas.token = "$HP_SHARED_KEY"
34+
35+
[[proxies]]
36+
name = "$APP_ID"
37+
type = "tcp"
38+
localIP = "127.0.0.1"
39+
localPort = $APP_PORT
40+
remotePort = $APP_PORT
41+
EOF
42+
fi
43+
else
44+
echo "HP_SHARED_KEY is not set. Skipping FRP configuration."
45+
fi
46+
fi
47+
48+
# If we have a configuration file and the shared key is present, start the FRP client
49+
if [ -f /frpc.toml ] && [ -n "$HP_SHARED_KEY" ]; then
50+
echo "Starting frpc in the background..."
51+
frpc -c /frpc.toml &
52+
fi
53+
54+
# Start the main Python application
55+
echo "Starting main application..."
56+
exec python3 main.py

test.sh

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/bin/bash
2+
set -e
3+
4+
docker container remove --force nc_app_app-skeleton-python || true
5+
6+
docker build -t nc_app_app-skeleton-python .
7+
8+
docker run --rm \
9+
-e HP_SHARED_KEY="mysecret" \
10+
-e HP_FRP_ADDRESS="nextcloud-appapi-harp" \
11+
-e HP_FRP_PORT="8782" \
12+
-e APP_HOST="127.0.0.1" \
13+
-e APP_PORT="23090" \
14+
-e APP_ID="app-skeleton-python" \
15+
-e APP_SECRET="12345" \
16+
-e APP_VERSION="1.0.0" \
17+
-e NEXTCLOUD_URL="http://nextcloud.local" \
18+
--name nc_app_app-skeleton-python \
19+
--network=master_default \
20+
nc_app_app-skeleton-python

0 commit comments

Comments
 (0)