DeviceServlet.java

/*
 * Copyright (C) 2020-2024 by Savoir-faire Linux
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
package net.jami.jams.server.servlets.api.auth.device;

import static net.jami.jams.server.Server.certificateAuthority;
import static net.jami.jams.server.Server.dataStore;

import com.google.gson.Gson;
import com.google.gson.JsonObject;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import net.jami.jams.common.annotations.ScopedServletMethod;
import net.jami.jams.common.objects.devices.Device;
import net.jami.jams.common.objects.requests.DeviceRegistrationRequest;
import net.jami.jams.common.objects.responses.DeviceRegistrationResponse;
import net.jami.jams.common.objects.responses.DeviceRevocationResponse;
import net.jami.jams.common.objects.user.AccessLevel;
import net.jami.jams.common.serialization.adapters.GsonFactory;
import net.jami.jams.common.serialization.tomcat.TomcatCustomErrorHandler;
import net.jami.jams.server.core.workflows.RegisterDeviceFlow;
import net.jami.jams.server.core.workflows.RevokeDeviceFlow;

import java.io.IOException;

@WebServlet("/api/auth/device/*")
public class DeviceServlet extends HttpServlet {
    private final Gson gson = GsonFactory.createGson();

    /**
     * @apiVersion 1.0.0
     * @api {get} /api/auth/device Get device info
     * @apiName getDevice
     * @apiGroup Device
     * @apiParam {path} deviceId id of the device
     * @apiSuccess (200) {body} Device device information
     * @apiSuccessExample {json} Success-Response: { "certificate":"pem_encoded_certificate",
     *     "displayName":"My Galaxy S8", "deviceId":"6aec6252ad", "revoked":true }
     * @apiError (500) {null} null Device could not be retrieved
     */
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        String username = req.getAttribute("username").toString();
        String deviceId = req.getPathInfo().replace("/", "");

        Device device =
                dataStore.getDeviceDao().getByDeviceIdAndOwner(deviceId, username).orElseThrow();

        if (certificateAuthority.getLatestCRL().get() != null)
            device.setRevoked(
                    certificateAuthority
                                    .getLatestCRL()
                                    .get()
                                    .getRevokedCertificate(
                                            device.getCertificate().getSerialNumber())
                            != null);
        else device.setRevoked(false);

        resp.getOutputStream().write(gson.toJson(device).getBytes());
        resp.flushBuffer();
    }

    /**
     * @apiVersion 1.0.0
     * @api {post} /api/auth/device Enroll a device
     * @apiName postDevice
     * @apiGroup Device
     * @apiParam {body} DeviceRegistrationRequest device registration request
     * @apiParamExample {json} Request-Example: { "csr":"pem_encoded_csr", "deviceName":"My Galaxy
     *     S8" }
     * @apiSuccess (200) {body} DeviceRegistrationResponse registration response
     * @apiError (500) {null} null Device could not be enrolled
     * @apiSuccessExample {json} Success-Response: {
     *     "certificateChain":"pem_encoded_certificate_chain", "displayName":"John Doe",
     *     "nameServer":"https://mydomain.com", "deviceReceipt": "device_receipt_object",
     *     "receiptSignature":"receipt_signature_object", "userPhoto":"base64_encoded_photo" }
     */
    @Override
    @ScopedServletMethod(securityGroups = {AccessLevel.USER})
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        Gson gson = GsonFactory.createGson();
        String username = req.getAttribute("username").toString();

        DeviceRegistrationRequest request =
                gson.fromJson(req.getReader(), DeviceRegistrationRequest.class);
        DeviceRegistrationResponse devResponse =
                RegisterDeviceFlow.registerDevice(username, request);

        if (devResponse == null) {
            TomcatCustomErrorHandler.sendCustomError(
                    resp, 500, "could not enroll a device due to server-side error");
            return;
        }

        String filteredJson = gson.toJson(devResponse);
        JsonObject obj = gson.fromJson(filteredJson, JsonObject.class);

        renameKeys(obj);

        resp.getOutputStream().write((obj.toString()).getBytes());
        resp.flushBuffer();
    }

    /**
     * Modifies in place obj to rename a key
     *
     * @param obj
     * @param oldKey
     * @param newKey
     */
    private static void renameKey(JsonObject obj, String oldKey, String newKey) {
        if (obj.get(oldKey) != null) {
            obj.add(newKey, obj.get(oldKey));
            obj.remove(oldKey);
        }
    }

    /**
     * Modifies in place obj to rename keys
     *
     * @param obj
     */
    public static void renameKeys(JsonObject obj) {
        renameKey(obj, "videoEnabled", "Account.videoEnabled");
        renameKey(obj, "publicInCalls", "DHTRelay.PublicInCalls");
        renameKey(obj, "autoAnswer", "Account.autoAnswer");
        renameKey(obj, "peerDiscovery", "Account.peerDiscovery");
        renameKey(obj, "accountDiscovery", "Account.accountDiscovery");
        renameKey(obj, "accountPublish", "Account.accountPublish");

        renameKey(obj, "rendezVous", "Account.rendezVous");
        renameKey(obj, "upnpEnabled", "Account.upnpEnabled");

        renameKey(obj, "turnEnabled", "TURN.enable");
        renameKey(obj, "turnServer", "TURN.server");
        renameKey(obj, "turnServerUserName", "TURN.username");
        renameKey(obj, "turnServerPassword", "TURN.password");
        renameKey(obj, "proxyEnabled", "Account.proxyEnabled");
        renameKey(obj, "proxyServer", "Account.proxyServer");
        renameKey(obj, "dhtProxyListUrl", "Account.dhtProxyListUrl");
        renameKey(obj, "displayName", "Account.displayName");
        renameKey(obj, "defaultModerators", "Account.defaultModerators");
        renameKey(obj, "uiCustomization", "Account.uiCustomization");
    }

    /**
     * @apiVersion 1.0.0
     * @api {put} /api/auth/device Change the name of a device
     * @apiName putDevice
     * @apiGroup Device
     * @apiParam {path} deviceId id of the device
     * @apiParam {query} deviceName new name for the device
     * @apiSuccess (200) {null} null name changed successfully
     * @apiError (500) {null} null device name could not be changed
     */
    @Override
    protected void doPut(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        String username = req.getAttribute("username").toString();
        String deviceId = req.getPathInfo().replace("/", "");
        String deviceName = req.getParameter("deviceName");

        if (dataStore.getDeviceDao().updateObject(deviceName, username, deviceId))
            resp.setStatus(200);
        else
            TomcatCustomErrorHandler.sendCustomError(
                    resp, 500, "could not update device information due to server-side error");
    }

    /**
     * @apiVersion 1.0.0
     * @api {delete} /api/auth/device Deactivate a device
     * @apiName deleteDevice
     * @apiGroup Device
     * @apiParam {path} deviceId id of the device
     * @apiSuccess (200) {null} null device successfully deactivated
     * @apiError (500) {null} null device could not be deactivated
     */
    @Override
    @ScopedServletMethod(securityGroups = {AccessLevel.USER})
    protected void doDelete(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        String deviceId = req.getPathInfo().replace("/", "");
        // If the device does not belong to the user throw a 403
        String owner = req.getAttribute("username").toString();

        long deviceIdCount =
                dataStore.getDeviceDao().getByOwner(owner).stream()
                        .filter(device -> device.getDeviceId().equals(deviceId))
                        .count();

        if (deviceIdCount == 0) {
            TomcatCustomErrorHandler.sendCustomError(
                    resp, 403, "You do not have sufficient rights to revoke this device!");
            return;
        }

        DeviceRevocationResponse devResponse = RevokeDeviceFlow.revokeDevice(owner, deviceId);
        if (devResponse != null) {
            resp.getOutputStream().write(gson.toJson(devResponse).getBytes());
            resp.flushBuffer();
        } else
            TomcatCustomErrorHandler.sendCustomError(
                    resp, 500, "could not revoke device due to server-side error");
    }
}