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

Nextcloud 32: HaRP support #34

Merged
merged 2 commits into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Declare files that always have LF line endings on checkout
* text eol=lf
23 changes: 22 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
FROM python:3.11-slim-bookworm

RUN apt-get update && apt-get install -y curl procps && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

COPY requirements.txt /

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

COPY --chmod=775 healthcheck.sh /
COPY --chmod=775 start.sh /

# Download and install FRP client
RUN set -ex; \
ARCH=$(uname -m); \
if [ "$ARCH" = "aarch64" ]; then \
FRP_URL="https://raw.githubusercontent.com/cloud-py-api/HaRP/main/exapps_dev/frp_0.61.1_linux_arm64.tar.gz"; \
else \
FRP_URL="https://raw.githubusercontent.com/cloud-py-api/HaRP/main/exapps_dev/frp_0.61.1_linux_amd64.tar.gz"; \
fi; \
echo "Downloading FRP client from $FRP_URL"; \
curl -L "$FRP_URL" -o /tmp/frp.tar.gz; \
tar -C /tmp -xzf /tmp/frp.tar.gz; \
mv /tmp/frp_0.61.1_linux_* /tmp/frp; \
cp /tmp/frp/frpc /usr/local/bin/frpc; \
chmod +x /usr/local/bin/frpc; \
rm -rf /tmp/frp /tmp/frp.tar.gz

WORKDIR /ex_app/lib
ENTRYPOINT ["python3", "main.py"]
ENTRYPOINT ["/start.sh"]
HEALTHCHECK --interval=2s --timeout=2s --retries=300 CMD /healthcheck.sh
31 changes: 29 additions & 2 deletions appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<description>
<![CDATA[Simplest skeleton of the Nextcloud application written in python]]>
</description>
<version>2.0.0</version>
<version>3.0.0</version>
<licence>MIT</licence>
<author mail="[email protected]" homepage="https://github.com/bigcat88">Alexander Piskun</author>
<namespace>PyAppV2_skeleton</namespace>
Expand All @@ -15,7 +15,7 @@
<bugs>https://github.com/nextcloud/app-skeleton-python/issues</bugs>
<repository type="git">https://github.com/nextcloud/app-skeleton-python</repository>
<dependencies>
<nextcloud min-version="29" max-version="31"/>
<nextcloud min-version="29" max-version="32"/>
</dependencies>
<external-app>
<docker-install>
Expand All @@ -36,5 +36,32 @@
<description>Test environment without default value</description>
</variable>
</environment-variables>
<routes>
<route>
<url>^/public$</url>
<verb>GET</verb>
<access_level>PUBLIC</access_level>
</route>
<route>
<url>^/user$</url>
<verb>GET</verb>
<access_level>USER</access_level>
</route>
<route>
<url>^/admin$</url>
<verb>GET</verb>
<access_level>ADMIN</access_level>
</route>
<route>
<url>^/$</url>
<verb>GET</verb>
<access_level>USER</access_level>
</route>
<route>
<url>^/ws$</url>
<verb>GET</verb>
<access_level>USER</access_level>
</route>
</routes>
</external-app>
</info>
112 changes: 109 additions & 3 deletions ex_app/lib/main.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
"""Simplest example."""

import asyncio
import datetime
import os
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Annotated

from fastapi import FastAPI
from fastapi import Depends, FastAPI, Request, WebSocket
from fastapi.responses import HTMLResponse, JSONResponse
from nc_py_api import NextcloudApp
from nc_py_api.ex_app import AppAPIAuthMiddleware, LogLvl, run_app, set_handlers
from nc_py_api.ex_app import AppAPIAuthMiddleware, LogLvl, nc_app, run_app, set_handlers


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

# Build the WebSocket URL dynamically using the NextcloudApp configuration.
WS_URL = NextcloudApp().app_cfg.endpoint + "/exapps/app-skeleton-python/ws"

# HTML content served at the root URL.
# This page opens a WebSocket connection, displays incoming messages,
# and allows you to send messages back to the server.
HTML = f"""
<!DOCTYPE html>
<html>
<head>
<title>FastAPI WebSocket Demo</title>
</head>
<body>
<h1>FastAPI WebSocket Demo</h1>
<p>Type a message and click "Send", or simply watch the server send cool updates!</p>
<input type="text" id="messageText" placeholder="Enter message here...">
<button onclick="sendMessage()">Send</button>
<ul id="messages"></ul>
<script>
// Create a WebSocket connection using the dynamic URL.
var ws = new WebSocket("{WS_URL}");

// When a message is received from the server, add it to the list.
ws.onmessage = function(event) {{
var messages = document.getElementById('messages');
var message = document.createElement('li');
message.textContent = event.data;
messages.appendChild(message);
}};

// Function to send a message to the server.
function sendMessage() {{
var input = document.getElementById("messageText");
ws.send(input.value);
input.value = '';
}}
</script>
</body>
</html>
"""


@APP.get("/")
async def get():
# WebSockets works only in Nextcloud 32 when `HaRP` is used instead of `DSP`
return HTMLResponse(HTML)


@APP.get("/public")
async def public_get(request: Request):
print(f"public_get: {request.headers}", flush=True)
return "Public page!"


@APP.get("/user")
async def user_get(request: Request, status: int = 200):
print(f"user_get: {request.headers}", flush=True)
return JSONResponse(content="Page for the registered users only!", status_code=status)


@APP.get("/admin")
async def admin_get(request: Request):
print(f"admin_get: {request.headers}", flush=True)
return "Admin page!"


@APP.websocket("/ws")
async def websocket_endpoint(
websocket: WebSocket,
nc: Annotated[NextcloudApp, Depends(nc_app)],
):
# WebSockets works only in Nextcloud 32 when `HaRP` is used instead of `DSP`
print(nc.user) # if you need user_id that initiated WebSocket connection
print(f"websocket_endpoint: {websocket.headers}", flush=True)
await websocket.accept()

# This background task sends a periodic message (the current time) every 2 seconds.
async def send_periodic_messages():
while True:
try:
message = f"Server time: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
await websocket.send_text(message)
await asyncio.sleep(2)
except Exception as exc:
NextcloudApp().log(LogLvl.ERROR, str(exc))
break

# Start the periodic sender in the background.
periodic_task = asyncio.create_task(send_periodic_messages())

try:
# Continuously listen for messages from the client.
while True:
data = await websocket.receive_text()
# Echo the received message back to the client.
await websocket.send_text(f"Echo: {data}")
except Exception as e:
NextcloudApp().log(LogLvl.ERROR, str(e))
finally:
# Cancel the periodic message task when the connection is closed.
periodic_task.cancel()


def enabled_handler(enabled: bool, nc: NextcloudApp) -> str:
# This will be called each time application is `enabled` or `disabled`
# NOTE: `user` is unavailable on this step, so all NC API calls that require it will fail as unauthorized.
print(f"enabled={enabled}")
print(f"enabled={enabled}", flush=True)
if enabled:
nc.log(LogLvl.WARNING, f"Hello from {nc.app_cfg.app_name} :)")
else:
Expand Down
8 changes: 7 additions & 1 deletion healthcheck.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
#!/bin/bash

exit 0
if [ -f /frpc.toml ] && [ -n "$HP_SHARED_KEY" ]; then
if pgrep -x "frpc" > /dev/null; then
exit 0
else
exit 1
fi
fi
56 changes: 56 additions & 0 deletions start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/bin/bash
set -e

# Check if the configuration file already exists
if [ -f /frpc.toml ]; then
echo "/frpc.toml already exists, skipping creation."
else
# Only create a config file if HP_SHARED_KEY is set.
if [ -n "$HP_SHARED_KEY" ]; then
echo "HP_SHARED_KEY is set, creating /frpc.toml configuration file..."
if [ -d "/certs/frp" ]; then
echo "Found /certs/frp directory. Creating configuration with TLS certificates."
cat <<EOF > /frpc.toml
serverAddr = "$HP_FRP_ADDRESS"
serverPort = $HP_FRP_PORT
metadatas.token = "$HP_SHARED_KEY"
transport.tls.certFile = "/certs/frp/client.crt"
transport.tls.keyFile = "/certs/frp/client.key"
transport.tls.trustedCaFile = "/certs/frp/ca.crt"

[[proxies]]
name = "$APP_ID"
type = "tcp"
localIP = "127.0.0.1"
localPort = $APP_PORT
remotePort = $APP_PORT
EOF
else
echo "Directory /certs/frp not found. Creating configuration without TLS certificates."
cat <<EOF > /frpc.toml
serverAddr = "$HP_FRP_ADDRESS"
serverPort = $HP_FRP_PORT
metadatas.token = "$HP_SHARED_KEY"

[[proxies]]
name = "$APP_ID"
type = "tcp"
localIP = "127.0.0.1"
localPort = $APP_PORT
remotePort = $APP_PORT
EOF
fi
else
echo "HP_SHARED_KEY is not set. Skipping FRP configuration."
fi
fi

# If we have a configuration file and the shared key is present, start the FRP client
if [ -f /frpc.toml ] && [ -n "$HP_SHARED_KEY" ]; then
echo "Starting frpc in the background..."
frpc -c /frpc.toml &
fi

# Start the main Python application
echo "Starting main application..."
exec python3 main.py