svnserver.ext.web.server.WebServer.java Source code

Java tutorial

Introduction

Here is the source code for svnserver.ext.web.server.WebServer.java

Source

/**
 * This file is part of git-as-svn. It is subject to the license terms
 * in the LICENSE file found in the top-level directory of this distribution
 * and at http://www.gnu.org/licenses/gpl-2.0.html. No part of git-as-svn,
 * including this file, may be copied, modified, propagated, or distributed
 * except according to the terms contained in the LICENSE file.
 */
package svnserver.ext.web.server;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.apache.http.HttpHeaders;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.RequestLog;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.server.handler.RequestLogHandler;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.servlet.ServletMapping;
import org.eclipse.jgit.util.Base64;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jose4j.jwe.JsonWebEncryption;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tmatesoft.svn.core.SVNException;
import ru.bozaro.gitlfs.server.ServerError;
import svnserver.auth.User;
import svnserver.auth.UserDB;
import svnserver.context.Shared;
import svnserver.context.SharedContext;
import svnserver.ext.web.config.WebServerConfig;
import svnserver.ext.web.token.EncryptionFactory;
import svnserver.ext.web.token.TokenHelper;

import javax.servlet.Servlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.StringWriter;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * Web server component
 *
 * @author Artem V. Navrotskiy <bozaro@users.noreply.github.com>
 */
public class WebServer implements Shared {
    @NotNull
    private static final Logger log = LoggerFactory.getLogger(WebServer.class);

    @NotNull
    public static final String DEFAULT_REALM = "Git as Subversion server";
    @NotNull
    public static final String AUTH_BASIC = "Basic ";
    @NotNull
    public static final String AUTH_TOKEN = "Bearer ";

    @NotNull
    private final SharedContext context;
    @Nullable
    private final Server server;
    @Nullable
    private final ServletHandler handler;
    @NotNull
    private final WebServerConfig config;
    @NotNull
    private final EncryptionFactory tokenFactory;
    @NotNull
    private final List<Holder> servlets = new CopyOnWriteArrayList<>();

    public WebServer(@NotNull SharedContext context, @Nullable Server server, @NotNull WebServerConfig config,
            @NotNull EncryptionFactory tokenFactory) {
        this.context = context;
        this.server = server;
        this.config = config;
        this.tokenFactory = tokenFactory;
        if (server != null) {
            final ServletContextHandler contextHandler = new ServletContextHandler();
            contextHandler.setContextPath("/");
            handler = contextHandler.getServletHandler();

            //final ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
            //securityHandler.addConstraintMapping(new );
            //contextHandler.setSecurityHandler(securityHandler);

            final RequestLogHandler logHandler = new RequestLogHandler();
            logHandler.setRequestLog(new RequestLog() {
                @Override
                public void log(Request request, Response response) {
                    final User user = (User) request.getAttribute(User.class.getName());
                    final String userName = (user == null || user.isAnonymous()) ? "" : user.getUserName();
                    log.info("{}:{} - {} - \"{} {}\" {} {}", request.getRemoteHost(), request.getRemotePort(),
                            userName, request.getMethod(), request.getHttpURI(), response.getStatus(),
                            response.getReason());
                }
            });

            final HandlerCollection handlers = new HandlerCollection();
            handlers.addHandler(contextHandler);
            handlers.addHandler(logHandler);
            server.setHandler(handlers);
        } else {
            handler = null;
        }
    }

    @NotNull
    public String getRealm() {
        return config.getRealm();
    }

    @NotNull
    public JsonWebEncryption createEncryption() {
        return tokenFactory.create();
    }

    @Override
    public void ready(@NotNull SharedContext context) throws IOException {
        try {
            if (server != null) {
                server.start();
            }
        } catch (Exception e) {
            throw new IOException("Can't start http server", e);
        }
    }

    @NotNull
    public Holder addServlet(@NotNull String pathSpec, @NotNull Servlet servlet) {
        log.info("Registered servlet for path: {}", pathSpec);
        final Holder servletInfo = new Holder(pathSpec, servlet);
        servlets.add(servletInfo);
        updateServlets();
        return servletInfo;
    }

    @NotNull
    public Collection<Holder> addServlets(@NotNull Map<String, Servlet> servletMap) {
        List<Holder> servletInfos = new ArrayList<>();
        for (Map.Entry<String, Servlet> entry : servletMap.entrySet()) {
            log.info("Registered servlet for path: {}", entry.getKey());
            final Holder servletInfo = new Holder(entry.getKey(), entry.getValue());
            servletInfos.add(servletInfo);
        }
        servlets.addAll(servletInfos);
        updateServlets();
        return servletInfos;
    }

    public void removeServlet(@NotNull Holder servletInfo) {
        if (servlets.remove(servletInfo)) {
            log.info("Unregistered servlet for path: {}", servletInfo.path);
            updateServlets();
        }
    }

    public void removeServlets(@NotNull Collection<Holder> servletInfos) {
        boolean modified = false;
        for (Holder servlet : servletInfos) {
            if (servlets.remove(servlet)) {
                log.info("Unregistered servlet for path: {}", servlet.path);
                modified = true;
            }
        }
        if (modified) {
            updateServlets();
        }
    }

    private void updateServlets() {
        if (handler != null) {
            final Holder[] snapshot = servlets.toArray(new Holder[servlets.size()]);
            final ServletHolder[] holders = new ServletHolder[snapshot.length];
            final ServletMapping[] mappings = new ServletMapping[snapshot.length];
            for (int i = 0; i < snapshot.length; ++i) {
                holders[i] = snapshot[i].holder;
                mappings[i] = snapshot[i].mapping;
            }
            handler.setServlets(holders);
            handler.setServletMappings(mappings);
        }
    }

    @Override
    public void close() throws Exception {
        if (server != null) {
            server.stop();
            server.join();
        }
    }

    @NotNull
    public static WebServer get(@NotNull SharedContext context) throws IOException {
        return context.getOrCreate(WebServer.class,
                () -> new WebServer(context, null, new WebServerConfig(), JsonWebEncryption::new));
    }

    /**
     * Return current user information.
     *
     * @param authorization HTTP authorization header value.
     * @return Return value:
     * <ul>
     * <li>no authorization header - anonymous user;</li>
     * <li>invalid authorization header - null;</li>
     * <li>valid authorization header - user information.</li>
     * </ul>
     */
    @Nullable
    public User getAuthInfo(@Nullable final String authorization) {
        final UserDB userDB = context.sure(UserDB.class);
        // Check HTTP authorization.
        if (authorization == null) {
            return User.getAnonymous();
        }
        if (authorization.startsWith(AUTH_BASIC)) {
            final String raw = new String(Base64.decode(authorization.substring(AUTH_BASIC.length()).trim()),
                    StandardCharsets.UTF_8);
            final int separator = raw.indexOf(':');
            if (separator > 0) {
                final String username = raw.substring(0, separator);
                final String password = raw.substring(separator + 1);
                try {
                    return userDB.check(username, password);
                } catch (IOException | SVNException e) {
                    log.error("Authorization error: " + e.getMessage(), e);
                }
            }
            return null;
        }
        if (authorization.startsWith(AUTH_TOKEN)) {
            return TokenHelper.parseToken(createEncryption(), authorization.substring(AUTH_TOKEN.length()).trim());
        }
        return null;
    }

    @NotNull
    public URI getUrl(@NotNull HttpServletRequest req) {
        if (config.getBaseUrl() != null) {
            return URI.create(config.getBaseUrl()).resolve(req.getRequestURI());
        }
        String host = req.getHeader(HttpHeaders.HOST);
        if (host == null) {
            host = req.getServerName() + ":" + req.getServerPort();
        }
        return URI.create(req.getScheme() + "://" + host + req.getRequestURI());
    }

    @NotNull
    public URI getUrl(@NotNull URI baseUri) {
        if (config.getBaseUrl() != null) {
            return URI.create(config.getBaseUrl()).resolve(baseUri.getPath());
        }
        return baseUri;
    }

    @NotNull
    public static ObjectMapper createJsonMapper() {
        final ObjectMapper mapper = new ObjectMapper();
        mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        return mapper;
    }

    public void sendError(@NotNull HttpServletRequest req, @NotNull HttpServletResponse resp,
            @NotNull ServerError error) throws IOException {
        resp.setContentType("text/html");
        resp.setStatus(error.getStatusCode());
        resp.getWriter().write(new ErrorWriter(req).content(error));
    }

    public final class Holder {
        @NotNull
        private final String path;
        @NotNull
        private final ServletHolder holder;
        @NotNull
        private final ServletMapping mapping;

        private Holder(@NotNull String pathSpec, @NotNull Servlet servlet) {
            path = pathSpec;
            holder = new ServletHolder(servlet);
            mapping = new ServletMapping();
            mapping.setServletName(holder.getName());
            mapping.setPathSpec(pathSpec);
        }

        public void removeServlet() {
            WebServer.this.removeServlet(this);
        }
    }

    private static class ErrorWriter extends ErrorHandler {

        private final HttpServletRequest req;

        public ErrorWriter(HttpServletRequest req) {
            this.req = req;
        }

        @NotNull
        public String content(@NotNull ServerError error) {
            try {
                final StringWriter writer = new StringWriter();
                writeErrorPage(req, writer, error.getStatusCode(), error.getMessage(), false);
                return writer.toString();
            } catch (IOException e) {
                return e.getMessage();
            }
        }
    }
}