ForkBootstrap.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.tika.pipes.core.protocol;

import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.security.SecureRandom;

import org.apache.tika.config.loader.TikaJsonConfig;
import org.apache.tika.config.loader.TikaObjectMapperFactory;
import org.apache.tika.exception.TikaConfigException;

/**
 * What the parent hands a forked PipesServer on its stdin before anything else: an auth
 * token and the parent's config. The fork presents the token as the first bytes of every
 * socket connection so the other end can reject a stranger that connected first.
 * <p>
 * Frame: {@value #TOKEN_LENGTH_BYTES}-byte token, int config length, config JSON.
 * The config's {@code ${env:...}} references are resolved by the parent, never by the fork.
 */
public final class ForkBootstrap {

    public static final int TOKEN_LENGTH_BYTES = 32;

    // Fixed: the configurable IPC limits live inside the config this frame carries.
    static final int MAX_CONFIG_BYTES = 64 * 1024 * 1024;

    private static final SecureRandom RANDOM = new SecureRandom();

    private final byte[] token;
    private final byte[] config;

    private ForkBootstrap(byte[] token, byte[] config) {
        this.token = token;
        this.config = config;
    }

    public static byte[] newToken() {
        byte[] token = new byte[TOKEN_LENGTH_BYTES];
        RANDOM.nextBytes(token);
        return token;
    }

    /**
     * Serializes the parent's loaded config. Its env references are already resolved, so the
     * bytes may carry secrets; they travel only over the fork's stdin.
     */
    public static byte[] toBytes(TikaJsonConfig tikaJsonConfig) throws IOException {
        return TikaObjectMapperFactory.getMapper().writeValueAsBytes(tikaJsonConfig.getRootNode());
    }

    public static void write(OutputStream out, byte[] token, byte[] config) throws IOException {
        DataOutputStream dos = new DataOutputStream(out);
        dos.write(token);
        dos.writeInt(config.length);
        dos.write(config);
        dos.flush();
    }

    public static ForkBootstrap read(InputStream in) throws IOException {
        DataInputStream dis = new DataInputStream(in);
        byte[] token = new byte[TOKEN_LENGTH_BYTES];
        dis.readFully(token);
        int length = dis.readInt();
        if (length < 0 || length > MAX_CONFIG_BYTES) {
            throw new IOException("bootstrap config length " + length + " is outside [0, "
                    + MAX_CONFIG_BYTES + "]");
        }
        byte[] config = new byte[length];
        dis.readFully(config);
        return new ForkBootstrap(token, config);
    }

    /**
     * Reads {@value #TOKEN_LENGTH_BYTES} bytes and compares them to {@code expected} in
     * constant time. False on a short read or a mismatch.
     */
    public static boolean checkToken(InputStream in, byte[] expected) throws IOException {
        byte[] presented = new byte[TOKEN_LENGTH_BYTES];
        try {
            new DataInputStream(in).readFully(presented);
        } catch (EOFException e) {
            return false;
        }
        return MessageDigest.isEqual(expected, presented);
    }

    public byte[] getToken() {
        return token;
    }

    /** Loads the config without resolving env references: the parent already did. */
    public TikaJsonConfig loadConfig() throws TikaConfigException {
        return TikaJsonConfig.load(new ByteArrayInputStream(config), false);
    }
}