package org.yamcs.web.rest; import static io.netty.handler.codec.http.HttpResponseStatus.OK; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.yamcs.Processor; import org.yamcs.YamcsServer; import org.yamcs.alarms.AlarmServer; import org.yamcs.api.MediaType; import org.yamcs.management.ManagementService; import org.yamcs.protobuf.SchemaWeb; import org.yamcs.protobuf.Web.RestExceptionMessage; import org.yamcs.protobuf.Yamcs.NamedObjectId; import org.yamcs.protobuf.YamcsManagement.ClientInfo; import org.yamcs.protobuf.YamcsManagement.LinkInfo; import org.yamcs.security.Privilege; import org.yamcs.utils.StringConverter; import org.yamcs.web.BadRequestException; import org.yamcs.web.HttpException; import org.yamcs.web.HttpRequestHandler; import org.yamcs.web.HttpUtils; import org.yamcs.web.NotFoundException; import org.yamcs.web.RouteHandler; import org.yamcs.xtce.Algorithm; import org.yamcs.xtce.MetaCommand; import org.yamcs.xtce.NameDescription; import org.yamcs.xtce.Parameter; import org.yamcs.xtce.SequenceContainer; import org.yamcs.xtce.XtceDb; import org.yamcs.yarch.Stream; import org.yamcs.yarch.TableDefinition; import org.yamcs.yarch.YarchDatabase; import com.google.protobuf.MessageLite; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.codec.http.LastHttpContent; import io.protostuff.Schema; /** * Contains utility methods for REST handlers. May eventually refactor this out. */ public abstract class RestHandler extends RouteHandler { private static final Logger log = LoggerFactory.getLogger(RestHandler.class); protected static void completeOK(RestRequest restRequest) { HttpResponse httpResponse = new DefaultFullHttpResponse(HTTP_1_1, OK); HttpUtil.setContentLength(httpResponse, 0); completeRequest(restRequest, httpResponse); } protected static <T extends MessageLite> void completeOK(RestRequest restRequest, T responseMsg, Schema<T> responseSchema) { HttpRequestHandler.sendMessageResponse(restRequest.getChannelHandlerContext(), restRequest.getHttpRequest(), OK, responseMsg, responseSchema).addListener(l -> { restRequest.getCompletableFuture().complete(null); }); } protected static void completeOK(RestRequest restRequest, MediaType contentType, ByteBuf body) { if (body == null) { throw new NullPointerException("body cannot be null; use the completeOK(request) to send an empty response."); } HttpResponse httpResponse = new DefaultFullHttpResponse(HTTP_1_1, OK, body); HttpUtils.setContentTypeHeader(httpResponse, contentType); int txSize = body.readableBytes(); HttpUtil.setContentLength(httpResponse, txSize); restRequest.addTransferredSize(txSize); completeRequest(restRequest, httpResponse); } private static void completeRequest(RestRequest restRequest, HttpResponse httpResponse) { ChannelFuture cf = HttpRequestHandler.sendResponse(restRequest.getChannelHandlerContext(), restRequest.getHttpRequest(), httpResponse, true); cf.addListener(l -> { restRequest.getCompletableFuture().complete(null); }); } protected static ChannelFuture sendRestError(RestRequest req, HttpResponseStatus status, Throwable t) { ChannelHandlerContext ctx = req.getChannelHandlerContext(); RestExceptionMessage msg = toException(t).build(); return HttpRequestHandler.sendMessageResponse(ctx, req.getHttpRequest(), status, msg, SchemaWeb.RestExceptionMessage.WRITE); } /** * write the error to the client and complete the request exceptionally * @param req * @param e */ protected static void completeWithError(RestRequest req, HttpException e) { ChannelFuture cf = sendRestError(req, e.getStatus(), e); cf.addListener(l-> { req.getCompletableFuture().completeExceptionally(e); }); } protected static void abortRequest(RestRequest req) { req.getCompletableFuture().complete(null); } /** * Just a little shortcut because builders are dead ugly */ private static RestExceptionMessage.Builder toException(Throwable t) { RestExceptionMessage.Builder exceptionb = RestExceptionMessage.newBuilder(); exceptionb.setType(t.getClass().getSimpleName()); if (t.getMessage() != null) { exceptionb.setMsg(t.getMessage()); } return exceptionb; } protected static String verifyInstance(RestRequest req, String instance) throws NotFoundException { if (!YamcsServer.hasInstance(instance)) { throw new NotFoundException(req, "No instance named '" + instance + "'"); } return instance; } protected static LinkInfo verifyLink(RestRequest req, String instance, String linkName) throws NotFoundException { verifyInstance(req, instance); LinkInfo linkInfo = ManagementService.getInstance().getLinkInfo(instance, linkName); if (linkInfo == null) { throw new NotFoundException(req, "No link named '" + linkName + "' within instance '" + instance + "'"); } return linkInfo; } protected static ClientInfo verifyClient(RestRequest req, int clientId) throws NotFoundException { ClientInfo ci = ManagementService.getInstance().getClientInfo(clientId); if (ci == null) { throw new NotFoundException(req, "No such client"); } else { return ci; } } protected static Processor verifyProcessor(RestRequest req, String instance, String processorName) throws NotFoundException { verifyInstance(req, instance); Processor processor = Processor.getInstance(instance, processorName); if (processor == null) { throw new NotFoundException(req, "No processor '" + processorName + "' within instance '" + instance + "'"); } else { return processor; } } protected static String verifyNamespace(RestRequest req, XtceDb mdb, String pathName) throws NotFoundException { if (mdb.getNamespaces().contains(pathName)) { return pathName; } String rooted = "/" + pathName; if (mdb.getNamespaces().contains(rooted)) { return rooted; } throw new NotFoundException(req, "No such namespace"); } protected static NamedObjectId verifyParameterId(RestRequest req, XtceDb mdb, String pathName) throws NotFoundException { return verifyParameterWithId(req, mdb, pathName).getRequestedId(); } protected static Parameter verifyParameter(RestRequest req, XtceDb mdb, String pathName) throws NotFoundException { return verifyParameterWithId(req, mdb, pathName).getItem(); } protected static NameDescriptionWithId<Parameter> verifyParameterWithId(RestRequest req, XtceDb mdb, String pathName) throws NotFoundException { int lastSlash = pathName.lastIndexOf('/'); if (lastSlash == -1 || lastSlash == pathName.length() - 1) { throw new NotFoundException(req, "No such parameter (missing namespace?)"); } String namespace = pathName.substring(0, lastSlash); String name = pathName.substring(lastSlash + 1); // First try with a prefixed slash (should be the common case) NamedObjectId id = NamedObjectId.newBuilder().setNamespace("/" + namespace).setName(name).build(); Parameter p = mdb.getParameter(id); if (p == null) { // Maybe some non-xtce namespace like MDB:OPS Name id = NamedObjectId.newBuilder().setNamespace(namespace).setName(name).build(); p = mdb.getParameter(id); } if (p != null && !authorised(req, Privilege.Type.TM_PARAMETER, p.getQualifiedName())) { log.warn("Parameter {} found, but withheld due to insufficient privileges. Returning 404 instead", StringConverter.idToString(id)); p = null; } if (p == null) { throw new NotFoundException(req, "No parameter named " + StringConverter.idToString(id)); } else { return new NameDescriptionWithId<Parameter>(p, id); } } protected static Stream verifyStream(RestRequest req, YarchDatabase ydb, String streamName) throws NotFoundException { Stream stream = ydb.getStream(streamName); if (stream == null) { throw new NotFoundException(req, "No stream named '" + streamName + "' (instance: '" + ydb.getName() + "')"); } else { return stream; } } protected static TableDefinition verifyTable(RestRequest req, YarchDatabase ydb, String tableName) throws NotFoundException { TableDefinition table = ydb.getTable(tableName); if (table == null) { throw new NotFoundException(req, "No table named '" + tableName + "' (instance: '" + ydb.getName() + "')"); } else { return table; } } protected static MetaCommand verifyCommand(RestRequest req, XtceDb mdb, String pathName) throws NotFoundException { int lastSlash = pathName.lastIndexOf('/'); if (lastSlash == -1 || lastSlash == pathName.length() - 1) { throw new NotFoundException(req, "No such command (missing namespace?)"); } String namespace = pathName.substring(0, lastSlash); String name = pathName.substring(lastSlash + 1); // First try with a prefixed slash (should be the common case) NamedObjectId id = NamedObjectId.newBuilder().setNamespace("/" + namespace).setName(name).build(); MetaCommand cmd = mdb.getMetaCommand(id); if (cmd == null) { // Maybe some non-xtce namespace like MDB:OPS Name id = NamedObjectId.newBuilder().setNamespace(namespace).setName(name).build(); cmd = mdb.getMetaCommand(id); } if (cmd != null && !authorised(req, Privilege.Type.TC, cmd.getQualifiedName())) { log.warn("Command {} found, but withheld due to insufficient privileges. Returning 404 instead", StringConverter.idToString(id)); cmd = null; } if (cmd == null) { throw new NotFoundException(req, "No such command"); } else { return cmd; } } protected static Algorithm verifyAlgorithm(RestRequest req, XtceDb mdb, String pathName) throws NotFoundException { int lastSlash = pathName.lastIndexOf('/'); if (lastSlash == -1 || lastSlash == pathName.length() - 1) { throw new NotFoundException(req, "No such algorithm (missing namespace?)"); } String namespace = pathName.substring(0, lastSlash); String name = pathName.substring(lastSlash + 1); // First try with a prefixed slash (should be the common case) NamedObjectId id = NamedObjectId.newBuilder().setNamespace("/" + namespace).setName(name).build(); Algorithm algorithm = mdb.getAlgorithm(id); if (algorithm != null) { return algorithm; } // Maybe some non-xtce namespace like MDB:OPS Name id = NamedObjectId.newBuilder().setNamespace(namespace).setName(name).build(); algorithm = mdb.getAlgorithm(id); if (algorithm != null) { return algorithm; } throw new NotFoundException(req, "No such algorithm"); } protected static SequenceContainer verifyContainer(RestRequest req, XtceDb mdb, String pathName) throws NotFoundException { int lastSlash = pathName.lastIndexOf('/'); if (lastSlash == -1 || lastSlash == pathName.length() - 1) { throw new NotFoundException(req, "No such container (missing namespace?)"); } String namespace = pathName.substring(0, lastSlash); String name = pathName.substring(lastSlash + 1); // First try with a prefixed slash (should be the common case) NamedObjectId id = NamedObjectId.newBuilder().setNamespace("/" + namespace).setName(name).build(); SequenceContainer container = mdb.getSequenceContainer(id); if (container != null) { return container; } // Maybe some non-xtce namespace like MDB:OPS Name id = NamedObjectId.newBuilder().setNamespace(namespace).setName(name).build(); container = mdb.getSequenceContainer(id); if (container != null) { return container; } throw new NotFoundException(req, "No such container"); } protected static AlarmServer verifyAlarmServer(Processor processor) throws BadRequestException { if (!processor.hasAlarmServer()) { String instance = processor.getInstance(); String processorName = processor.getName(); throw new BadRequestException("Alarms are not enabled for processor '" + instance + "/" + processorName + "'"); } else { return processor.getParameterRequestManager().getAlarmServer(); } } protected static boolean authorised(RestRequest req, Privilege.Type type, String privilege) { return Privilege.getInstance().hasPrivilege1(req.getAuthToken(), type, privilege); } protected static class NameDescriptionWithId<T extends NameDescription> { final private T item; private final NamedObjectId requestedId; NameDescriptionWithId(T item, NamedObjectId requestedId) { this.item = item; this.requestedId = requestedId; } public T getItem() { return item; } public NamedObjectId getRequestedId() { return requestedId; } } public static void completeChunkedTransfer(RestRequest req) { req.getChannelHandlerContext().writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT) .addListener(ChannelFutureListener.CLOSE) .addListener(l-> req.getCompletableFuture().complete(null)); } }