#StackBounty: #java #security #cryptography #aes JSafebox – Encrypted vault – 2nd drill

Bounty: 50

I am developing JSafebox, a portable safebox project written in Java.

A first review have been completed here.

The goal is to provide a secured environment where the user can browse encrypted files without leaking data on the drive.

The project is highly focused on security and I would appreciate any feedback that would help improve the quality of the code.

Core logic and sensitive method are located in below classes but feedback from other classes is welcome as well 🙂

org/ortis/jsafebox/Safe.java

/*******************************************************************************
 * Copyright 2018 Ortis (cao.ortis.org@gmail.com)
 * 
 * Licensed 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.ortis.jsafebox;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.lang.reflect.Type;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.Key;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.CancellationException;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.ortis.jsafebox.hash.Hasher;
import org.ortis.jsafebox.hash.SHA256;
import org.ortis.jsafebox.task.TaskProbe;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

/**
 * Virtual vault where files are stored
 * 
 * @author Ortis <br>
 *         2018 Apr 26 7:29:29 PM <br>
 */
public class Safe implements Closeable
{
    public static final String VERSION = "0.2 beta";

    public static final Gson GSON = new Gson();
    private static final Type MAP_STRING_STRING_TYPE = new TypeToken<Map<String, String>>()
    {
    }.getType();

    public static final Type BYTE_ARRAY_TYPE = new TypeToken<byte []>()
    {
    }.getType();

    private final static Hasher HASHER = new SHA256();

    public static final String ENCRYPTION_LABEL = "encryption";
    public final static String ENCRYPTION_IV_LENGTH_LABEL = "iv length";
    public static final String KEY_ALGO_LABEL = "algo";
    public static final String PROTOCOL_SPEC_LABEL = "protocol description";
    public static final String PBKDF2_SALT_LABEL = "pbkdf2 salt";
    public static final int PBKDF2_ITERATION = 100000;
    public static final String PBKDF2_ITERATION_LABEL = "pbkdf2 iteration";

    public static final String PROTOCOL_SPEC = "JSafebox is using a very simple protocol so encrypted files can be easily read by another program, as long as you have the password. The encryption key is derived from the password using PBKDF2 hashing with 100000 iteration. A JSafebox file contains a SHA256 integrity hash followed by blocks: [ integrity hash | block 0 | block 1 | ... | block N ]. Each block is stored as followed: [ IV | metadata length | metadata | data length | data ] where 'IV' is the Initialization_vector of the encryption (16 bytes), 'metadata' is a JSON string and 'length' are 64 bits (8 bytes) integer. The first block 'block 0' is the 'header' and is the only block not encrypted and therefore, the only block without IV. The 'header' only have metadata ('data length' is 0) and contains text entries specified by the user and various additional entries including a protocol explanation, the type of encoding and the parameters of the encryption. The 'header's metadata is stored as JSON string and can be seen by opening the safe file with a basic text editor. The second block 'block 1' is the 'properties'. It is similar to the 'header' except that it is encrypted and have an IV. The 'properties' contains text entries specified by the user and stored in JSON. The following blocks (from 2 to N) are the encrypted files. (Full manual at https://github.com/0rtis/jsafebox)";

    private final File originalFile;

    private final SecretKey encryptionKey;
    private final int ivLength;
    private final RandomAccessFile original;

    private final File tempFile;
    private final RandomAccessFile temp;

    private final byte [] hash;

    private final Map<String, String> publicHeader;
    private final Map<String, String> privateProperties;
    private final Map<String, Block> roBlocks;
    private final Map<String, Block> blocks;
    private final Map<String, Block> tempBlocks;
    private final Map<String, Block> deletedBlocks;

    private final int bufferSize;

    private final Folder root;

    /**
     * Create an instance of {@link Safe}
     * 
     * @param file:
     *            the safe file
     * @param cipher:
     *            cipher to decrypt the data
     * @param keySpec:
     *            key specification
     * @param algoSpec:
     *            encryption specification
     * @param bufferSize:
     *            size of the <code>byte</code> buffer to be used in IO operation
     * @throws Exception
     */
    public Safe(final File file, final SecretKey key, final int bufferSize) throws Exception
    {

        this.originalFile = file.getAbsoluteFile();

        this.encryptionKey = key;

        this.bufferSize = bufferSize;

        this.original = new RandomAccessFile(file, "rw");
        this.tempFile = Files.createTempFile(null, null).toFile();
        this.temp = new RandomAccessFile(this.tempFile, "rw");

        final HashMap<String, String> publicProps = new LinkedHashMap<>();
        this.publicHeader = Collections.unmodifiableMap(publicProps);
        final HashMap<String, String> props = new LinkedHashMap<>();
        this.privateProperties = Collections.unmodifiableMap(props);
        this.blocks = new LinkedHashMap<>();
        this.roBlocks = Collections.unmodifiableMap(blocks);
        this.tempBlocks = new LinkedHashMap<>();
        this.deletedBlocks = new LinkedHashMap<>();
        this.root = new Folder(null, Folder.ROOT_NAME);

        final byte [] buffer = new byte[bufferSize];
        final byte [] outBuffer = new byte[bufferSize];

        this.original.read(buffer, 0, HASHER.getHashLength());
        this.hash = new byte[HASHER.getHashLength()];
        System.arraycopy(buffer, 0, this.hash, 0, this.hash.length);

        long length;
        int read;
        final ByteArrayOutputStream baos = new ByteArrayOutputStream(buffer.length);

        final long headerLength = this.original.readLong();
        length = headerLength;

        while (length > 0)
        {

            if (length < buffer.length)
                read = this.original.read(buffer, 0, (int) length);
            else
                read = this.original.read(buffer);

            baos.write(buffer, 0, read);
            length -= read;

        }

        String json = new String(baos.toByteArray(), StandardCharsets.UTF_8);

        publicProps.putAll(GSON.fromJson(json, MAP_STRING_STRING_TYPE));

        this.ivLength = Integer.parseInt(this.publicHeader.get(ENCRYPTION_IV_LENGTH_LABEL));

        this.original.readLong();// data length 0

        // init cipher
        final Cipher cipher = getCipher();

        // read private properties
        this.original.read(buffer, 0, this.ivLength);// read properties iv
        IvParameterSpec iv = new IvParameterSpec(Arrays.copyOf(buffer, this.ivLength));
        cipher.init(Cipher.DECRYPT_MODE, this.encryptionKey, iv);

        final long propLength = this.original.readLong();
        length = propLength;

        baos.reset();

        while (length > 0)
        {

            if (length < buffer.length)
                read = this.original.read(buffer, 0, (int) length);
            else
                read = this.original.read(buffer);

            final int decrypted = cipher.update(buffer, 0, read, outBuffer);

            baos.write(outBuffer, 0, decrypted);
            length -= read;

        }

        baos.write(cipher.doFinal());

        json = new String(baos.toByteArray());

        props.putAll(GSON.fromJson(json, MAP_STRING_STRING_TYPE));
        this.original.readLong();// data length 0

        while (this.original.getFilePointer() < this.original.length())
        {
            baos.reset();

            final long offset = this.original.getFilePointer();

            this.original.read(buffer, 0, this.ivLength);// read properties iv
            iv = new IvParameterSpec(Arrays.copyOf(buffer, this.ivLength));
            cipher.init(Cipher.DECRYPT_MODE, this.encryptionKey, iv);

            final long metaLength = this.original.readLong();
            final long metaOffset = this.original.getFilePointer();

            length = metaLength;
            while (length > 0)
            {

                if (length < buffer.length)
                    read = this.original.read(buffer, 0, (int) length);
                else
                    read = this.original.read(buffer);

                final int decrypted = cipher.update(buffer, 0, read, outBuffer);
                baos.write(outBuffer, 0, decrypted);
                length -= read;
            }
            baos.write(cipher.doFinal());
            json = new String(baos.toByteArray());

            final Map<String, String> properties = new HashMap<>(GSON.fromJson(json, MAP_STRING_STRING_TYPE));
            final String path = properties.get(Block.PATH_LABEL);
            if (path == null)
                throw new IllegalStateException("Path of block starting at " + offset + " is not set");

            if (blocks.containsKey(path.toUpperCase(Environment.getLocale())))
                throw new IllegalStateException("Block path " + path + " already exist");

            final long dataLength = this.original.readLong();
            final long dataOffset = this.original.getFilePointer();

            final String [] tokens = path.split(Folder.REGEX_DELIMITER);

            this.root.mkdir(tokens, 1, true);

            final org.ortis.jsafebox.SafeFile dstFile;

            if (tokens.length == 2)
                dstFile = this.root;
            else
                dstFile = this.root.get(tokens, 1, tokens.length - 1);

            if (dstFile == null)
                throw new Exception("Could not find destination folder for block path " + path);

            if (!dstFile.isFolder())
                throw new Exception("Destination folder " + dstFile + " is a block");

            final Folder destinationFolder = ((Folder) dstFile);

            final long blockLength = original.getFilePointer() - offset + dataLength;
            final Block block = new Block(path, properties, offset, blockLength, metaOffset, metaLength, dataOffset, dataLength, destinationFolder);

            destinationFolder.add(block);

            blocks.put(block.getComparablePath(), block);
            this.original.seek(block.getOffset() + block.getLength());
        }

    }

    /**
     * Add data into the {@link Safe}. <b>Note that the data will be stored into the temporary safe file</b>. Use {@link Safe#save()} to save all temporary data
     * 
     * @param properties:
     *            metadata
     * @param data:
     *            data to encrypt
     * @return
     * @throws Exception
     */
    public synchronized Block add(final Map<String, String> properties, final InputStream data, TaskProbe probe) throws Exception
    {
        if (probe == null)
            probe = TaskProbe.DULL_PROBE;

        try
        {
            final String path = properties.get(Block.PATH_LABEL);

            if (path == null)
                throw new IllegalArgumentException("Property " + Block.PATH_LABEL + " is missing");

            org.ortis.jsafebox.SafeFile destinationFile = SafeFiles.get(path, this.root, this.root);

            if (destinationFile != null)
                throw new Exception("Block file " + destinationFile + " already exist");

            final String comparablePath = properties.get(Block.PATH_LABEL).toUpperCase(Environment.getLocale());

            final String [] comparableTokens = comparablePath.split(Folder.REGEX_DELIMITER);

            if (comparableTokens.length == 2 && root.getComparableName().equals(comparableTokens[0]))
                destinationFile = this.root;
            else
                destinationFile = this.root.get(comparableTokens, 1, comparableTokens.length - 1);

            if (destinationFile == null)
                throw new Exception("Destination folder " + destinationFile + " does not exists");

            if (!destinationFile.isFolder())
                throw new Exception("Destination " + destinationFile + " is not a folder");

            final Folder destinationFolder = (Folder) destinationFile;

            if (this.roBlocks.containsKey(path) || this.tempBlocks.containsKey(path))
                throw new Exception("Block path " + path + " already exist");

            final String name = properties.get(Block.NAME_LABEL);

            if (name == null)
                throw new IllegalArgumentException("Property " + Block.NAME_LABEL + " is missing");

            final Cipher cipher = getCipher();

            if (probe.isCancelRequested())
            {
                probe.fireCanceled();
                throw new CancellationException();
            }

            cipher.init(Cipher.ENCRYPT_MODE, this.encryptionKey, getSecureRandom());

            final RandomAccessFile temp = getTemp();

            final long offset = temp.getFilePointer();

            temp.write(cipher.getIV());

            // write metadata

            temp.writeLong(0);
            final String metadataserial = GSON.toJson(properties);
            final byte [] metaBuffer = metadataserial.getBytes();
            final long metaOffset = temp.getFilePointer();
            final long metaLength = encrypt(new ByteArrayInputStream(metaBuffer), cipher, temp, this.bufferSize, probe);
            long position = temp.getFilePointer();

            temp.seek(offset + cipher.getIV().length);
            temp.writeLong(metaLength);
            temp.seek(position);

            // write data
            position = temp.getFilePointer();
            temp.writeLong(0);

            final long dataOffset = temp.getFilePointer();

            final long dataLength = encrypt(data, cipher, temp, this.bufferSize, probe);

            temp.seek(position);
            temp.writeLong(dataLength);
            temp.seek(temp.length());

            final Block block = new Block(path, properties, offset, temp.getFilePointer() - offset, metaOffset, metaLength, dataOffset, dataLength, destinationFolder);
            this.tempBlocks.put(block.getComparablePath(), block);

            destinationFolder.add(block);

            return block;

        } catch (final CancellationException e)
        {
            throw e;
        } catch (final Exception e)
        {
            probe.fireException(e);
            throw e;
        } finally
        {
            probe.fireTerminated();
        }
    }

    /**
     * Delete data from the {@link Safe}. <b>Note that the data wont be deleted until a call to {@link Safe#save()} is made</b>
     * 
     * @param path:
     *            path of the data to delete
     */
    public  synchronized  void delete(final String path)
    {

        final String comparablePath = path.toUpperCase(Environment.getLocale());
        Block deleted = this.blocks.get(comparablePath);

        if (deleted != null)
        {
            final Folder folder = deleted.getParent();
            folder.remove(deleted.getName());
            this.deletedBlocks.put(comparablePath, deleted);
        }

        deleted = this.tempBlocks.remove(comparablePath);

        if (deleted != null)
        {
            final Folder folder = deleted.getParent();
            folder.remove(deleted.getName());
            this.deletedBlocks.put(comparablePath, deleted);
        }
    }

    /**
     * Extract data from the {@link Safe}
     * 
     * @param block:
     *            block to extract
     * @param outputStream:
     *            destination of extracted block
     * @throws Exception
     */
    public void extract(final Block block, final OutputStream outputStream) throws Exception
    {
        extract(block.getPath(), outputStream);
    }

    /**
     * Extract data from the {@link Safe}
     * 
     * @param path:
     *            path of the block to extract
     * @param outputStream:
     *            destination of extracted block
     * @throws Exception
     */
    public  synchronized  void extract(String path, final OutputStream outputStream) throws Exception
    {

        path = path.toUpperCase(Environment.getLocale());

        Block block = this.roBlocks.get(path);

        final RandomAccessFile raf;
        if (block == null)
        {
            block = this.tempBlocks.get(path);
            raf = this.temp;
        } else
            raf = this.original;

        if (block == null)
            throw new Exception("Block " + path + " not found");

        raf.seek(block.getOffset());

        final byte [] ivBytes = new byte[this.ivLength];
        raf.read(ivBytes);

        final Cipher cipher = getCipher();

        final IvParameterSpec iv = new IvParameterSpec(ivBytes);

        cipher.init(Cipher.DECRYPT_MODE, this.encryptionKey, iv);
        raf.seek(block.getDataOffset());
        decrypt(raf, block.getDataLength(), cipher, outputStream, this.bufferSize);

    }

    /**
     * Read the metadata of a {@link Block}
     * 
     * @param block:
     *            block to read
     * @return
     * @throws Exception
     */
    public  synchronized  Map<String, String> readMetadata(final Block block) throws Exception
    {

        this.original.seek(block.getOffset());
        final byte [] ivBytes = new byte[this.ivLength];
        this.original.read(ivBytes);

        final Cipher cipher = getCipher();

        final IvParameterSpec iv = new IvParameterSpec(ivBytes);
        cipher.init(Cipher.DECRYPT_MODE, this.encryptionKey, iv);

        this.original.seek(block.getMetaOffset());

        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
        decrypt(this.original, block.getMetaLength(), cipher, baos, this.bufferSize);

        final String metadata = new String(baos.toByteArray());

        final Map<String, String> jsonMap = GSON.fromJson(metadata, MAP_STRING_STRING_TYPE);
        return new TreeMap<>(jsonMap);
    }

    /**
     * Discard pending modification
     */
    public  synchronized  void discardChanges() throws Exception
    {

        for (final Map.Entry<String, Block> temp : this.tempBlocks.entrySet())
        {

            Folder folder = temp.getValue().getParent();
            folder.remove(temp.getValue().getName());
        }

        this.tempBlocks.clear();

        for (final Map.Entry<String, Block> deleted : this.deletedBlocks.entrySet())
        {
            Folder folder = deleted.getValue().getParent();
            folder.add(deleted.getValue());
        }
        this.deletedBlocks.clear();

    }

    /**
     * Save the modification into the safe file. The current file is renamed and a new file is written. This is to reduce the risk of data loss. This method calls the {@link Safe#close()} before returning
     * 
     * @return
     * @throws Exception
     */
    public Safe save() throws Exception
    {
        return save(null);
    }

    public  synchronized  Safe save(TaskProbe probe) throws Exception
    {

        if (probe == null)
            probe = TaskProbe.DULL_PROBE;
        try
        {
            double progress = 0;
            probe.fireProgress(progress);

            probe.fireMessage("Creating temporary file");
            final File newFile = Files.createTempFile(originalFile.getParentFile().toPath(), null, null).toFile();

            try (RandomAccessFile destination = new RandomAccessFile(newFile, "rw"))
            {

                Cipher cipher = getCipher();

                if (probe.isCancelRequested())
                {
                    probe.fireCanceled();
                    throw new CancellationException();
                }

                destination.write(HASHER.getEmptyHash());// skip hash

                // public properties
                probe.fireMessage("Writing public header");

                String json = GSON.toJson(this.publicHeader);

                long previousPosition = destination.getFilePointer();

                destination.writeLong(0);
                long total = write(new ByteArrayInputStream(json.getBytes()), destination, this.bufferSize, probe);
                destination.writeLong(0);// no data in header
                long position = destination.getFilePointer();
                destination.seek(previousPosition);
                destination.writeLong(total);
                destination.seek(position);

                if (probe.isCancelRequested())
                {
                    probe.fireCanceled();
                    throw new CancellationException();
                }

                // private properties
                probe.fireMessage("Writing private properties");

                cipher.init(Cipher.ENCRYPT_MODE, this.encryptionKey, getSecureRandom());
                destination.write(cipher.getIV());

                json = GSON.toJson(this.privateProperties);

                previousPosition = destination.getFilePointer();
                destination.writeLong(0);
                total = encrypt(new ByteArrayInputStream(json.getBytes()), cipher, destination, this.bufferSize, probe);
                destination.writeLong(0);// no data in header
                position = destination.getFilePointer();
                destination.seek(previousPosition);
                destination.writeLong(total);
                destination.seek(position);

                if (probe.isCancelRequested())
                {
                    probe.fireCanceled();
                    throw new CancellationException();
                }

                final double steps = this.roBlocks.size() + this.tempBlocks.size() + 1;
                int completed = 0;

                for (final Block block : this.roBlocks.values())
                {
                    // add non deleted only
                    if (this.deletedBlocks.containsKey(block.getComparablePath()))
                    {
                        probe.fireMessage("Skipping deleted block " + block.getPath());
                        continue;
                    }

                    probe.fireMessage("Writing block " + block.getPath());
                    this.original.seek(block.getOffset());
                    write(this.original, block.getLength(), destination, this.bufferSize, probe);
                    completed++;
                    progress = completed / steps;
                    probe.fireProgress(progress);
                }

                final RandomAccessFile temp = getTemp();
                for (final Block block : this.tempBlocks.values())
                {

                    if (this.deletedBlocks.containsKey(block.getComparablePath()))
                    {
                        probe.fireMessage("Skipping deleted block " + block.getPath());
                        continue;
                    }

                    probe.fireMessage("Writing block " + block.getPath());
                    temp.seek(block.getOffset());

                    write(temp, block.getLength(), destination, this.bufferSize, probe);
                    completed++;
                    progress = completed / steps;
                    probe.fireProgress(progress);

                }

                probe.fireMessage("Computing hash");
                final byte [] hash = computeHash(destination, cipher, this.ivLength, this.encryptionKey, this.bufferSize, probe);
                destination.seek(0);
                destination.write(hash);

                probe.fireMessage("Closing IO streams");
                destination.close();

                close();

                probe.fireMessage("Deleting old file");

                if (!this.originalFile.delete())
                    throw new IOException("Unable to delete " + this.originalFile.getAbsolutePath());

                probe.fireMessage("Renaming file");

                if (!newFile.renameTo(this.originalFile))
                    throw new IOException("Unable to rename " + newFile.getAbsolutePath());

                if (probe.isCancelRequested())
                {
                    probe.fireCanceled();
                    throw new CancellationException();
                }

                probe.fireMessage("Opening new safe");
                probe.fireProgress(1);

                return new Safe(this.originalFile, encryptionKey, this.bufferSize);
            }
        } catch (final CancellationException e)
        {
            throw e;
        } catch (final Exception e)
        {
            probe.fireException(e);
            throw e;
        } finally
        {
            probe.fireTerminated();
        }

    }

    /**
     * Compute the hash of the {@link Safe}
     * 
     * @return
     * @throws Exception
     */
    public  synchronized  byte [] computeHash(final TaskProbe probe) throws Exception
    {
        final byte [] hash = computeHash(this.original, getCipher(), this.ivLength, this.encryptionKey, this.bufferSize, probe);
        return hash;
    }

    /**
     * Return a copy of the hash that was in the {@link Safe}'s file
     * 
     * @return
     */
    public byte [] getHash()
    {
        final byte [] destination = new byte[this.hash.length];
        System.arraycopy(this.hash, 0, destination, 0, this.hash.length);
        return destination;
    }

    private Cipher getCipher() throws Exception
    {
        final String encryption = this.publicHeader.get(ENCRYPTION_LABEL);

        if (encryption == null)
            throw new Exception("Public property '" + ENCRYPTION_LABEL + "' must be set");

        return javax.crypto.Cipher.getInstance(encryption);
    }

    @Override
    public  synchronized  void close() throws IOException
    {
        this.original.close();

        final RandomAccessFile temp = getTemp();
        if (temp != null)
        {
            temp.close();
            tempFile.delete();
        }

    }

    /**
     * Get the properties of the {@link Safe}
     * 
     * @return
     */
    public Map<String, String> getPrivateProperties()
    {
        return privateProperties;
    }

    /**
     * Get the header of the {@link Safe}
     * 
     * @return
     */
    public Map<String, String> getPublicHeader()
    {
        return publicHeader;
    }

    /**
     * Get all {@link Block} contained in the {@link Safe}
     * 
     * @return
     */
    public Map<String, Block> getBlocks()
    {
        return this.roBlocks;
    }

    /**
     * Get a {@link Block} from the {@link Safe}
     * 
     * @param path:
     *            path of the {@link Block} to retrieve
     * @return
     */
    public Block getBlock(final String path)
    {

        final String comparablePath = path.toUpperCase(Environment.getLocale());

        return this.roBlocks.get(comparablePath);

    }

    /**
     * Get a {@link Block} from the temporary {@link Safe}
     * 
     * @param path:
     *            path of the {@link Block} to retrieve
     * @return
     */
    public Block getTempBlock(final String path)
    {
        final String comparablePath = path.toUpperCase(Environment.getLocale());

        return this.tempBlocks.get(comparablePath);

    }

    /**
     * Get all {@link Block} contained in the temporary {@link Safe}
     * 
     * @return
     */
    public Map<String, Block> getTempBlocks()
    {
        return tempBlocks;
    }

    /**
     * Get deleted {@link Block}
     * 
     * @return
     */
    public Map<String, Block> getDeletedBlocks()
    {
        return deletedBlocks;
    }

    /**
     * Get root {@link Folder}
     * 
     * @return
     */
    public Folder getRootFolder()
    {
        return root;
    }

    public File getFile()
    {
        return this.originalFile;
    }

    /**
     * Get the temporary safe file
     * 
     * @return
     */
    public File getTempFile()
    {
        return tempFile;
    }

    /**
     * Get the temporary safe file
     * 
     * @return
     */
    public RandomAccessFile getTemp() throws IOException
    {
        return this.temp;
    }

    private static long encrypt(final InputStream data, final Cipher cipher, final RandomAccessFile destination, final int bufferSize, final TaskProbe probe) throws Exception
    {

        final byte [] buffer = new byte[bufferSize];
        final byte [] bufferOut = new byte[bufferSize];

        long total = 0;
        int read;
        while ((read = data.read(buffer)) > -1)
        {

            read = cipher.update(buffer, 0, read, bufferOut);
            if (read == 0)
                // data length is less than cipher block size
                System.arraycopy(buffer, 0, bufferOut, 0, buffer.length);

            total += read;
            destination.write(bufferOut, 0, read);

            if (probe.isCancelRequested())
            {
                probe.fireCanceled();
                throw new CancellationException();
            }

        }

        read = cipher.doFinal(bufferOut, 0);
        destination.write(bufferOut, 0, read);
        total += read;

        return total;

    }

    private static void decrypt(final RandomAccessFile source, final long length, final Cipher cipher, final OutputStream destination, final int bufferSize) throws Exception
    {

        final byte [] buffer = new byte[bufferSize];
        final byte [] bufferOut = new byte[bufferSize];

        long remaining = length;
        int read;
        while (remaining > 0)
        {
            if (remaining < buffer.length)
                read = source.read(buffer, 0, (int) remaining);
            else
                read = source.read(buffer, 0, buffer.length);

            remaining -= read;

            read = cipher.update(buffer, 0, read, bufferOut);
            destination.write(bufferOut, 0, read);

        }

        read = cipher.doFinal(bufferOut, 0);
        destination.write(bufferOut, 0, read);

    }

    private static long write(final InputStream data, final RandomAccessFile destination, final int bufferSize, final TaskProbe probe) throws Exception
    {

        final byte [] buffer = new byte[bufferSize];

        long total = 0;
        int read;
        while ((read = data.read(buffer)) > -1)
        {
            destination.write(buffer, 0, read);
            total += read;

            if (probe.isCancelRequested())
            {
                probe.fireCanceled();
                throw new CancellationException();
            }
        }

        return total;

    }

    private static void write(final RandomAccessFile source, final long length, final RandomAccessFile destination, final int bufferSize, final TaskProbe probe) throws Exception
    {

        final byte [] buffer = new byte[bufferSize];

        long remaining = length;
        int read;
        while (remaining > 0)
        {
            if (remaining < buffer.length)
                read = source.read(buffer, 0, (int) remaining);
            else
                read = source.read(buffer, 0, buffer.length);

            destination.write(buffer, 0, read);

            remaining -= read;

            if (probe.isCancelRequested())
            {
                probe.fireCanceled();
                throw new CancellationException();
            }
        }

    }

    private static SecureRandom getSecureRandom()
    {
        return new SecureRandom();
    }

    /**
     * Read the header of the {@link Safe}
     * 
     * @param file:
     *            safe file to read
     * @param bufferSize:
     *            size of the <code>byte</code> buffer to be used in IO operation
     * @return
     * @throws IOException
     */
    public static Map<String, String> readHeader(final File file, final int bufferSize) throws IOException
    {
        RandomAccessFile raf = null;

        try
        {
            raf = new RandomAccessFile(file, "rw");
            final byte [] buffer = new byte[bufferSize];
            final ByteArrayOutputStream baos = new ByteArrayOutputStream(buffer.length);

            raf.read(buffer, 0, HASHER.getHashLength());// skip hash

            long length = raf.readLong();
            int read;
            while (length > 0)
            {

                if (length < buffer.length)
                    read = raf.read(buffer, 0, (int) length);
                else
                    read = raf.read(buffer);

                baos.write(buffer, 0, read);
                length -= read;

            }
            final String header = new String(baos.toByteArray(), StandardCharsets.UTF_8);
            return GSON.fromJson(header, MAP_STRING_STRING_TYPE);

        } finally
        {
            if (raf != null)
                raf.close();
        }

    }

    /**
     * Compute the hash value of {@link Safe} file
     * 
     * @param safeFile
     * @param cipher
     * @param ivLength
     * @param encryptionKey
     * @param bufferSize
     * @return
     * @throws Exception
     */
    public static byte [] computeHash(final RandomAccessFile safeFile, final Cipher cipher, final int ivLength, final Key encryptionKey, final int bufferSize, TaskProbe probe) throws Exception
    {

        if (probe == null)
            probe = TaskProbe.DULL_PROBE;

        try
        {
            final long previousPosition = safeFile.getFilePointer();

            final byte [] buffer = new byte[bufferSize];
            final byte [] bufferOut = new byte[bufferSize];


            safeFile.seek(HASHER.getHashLength());
            final ByteBuffer byteBuffer = ByteBuffer.allocate((int) (safeFile.length() - safeFile.getFilePointer()));

            long length = safeFile.readLong();
            byteBuffer.putLong(length);

            int read;

            // header
            while (length > 0)// read header
            {

                if (length < buffer.length)
                    read = safeFile.read(buffer, 0, (int) length);
                else
                    read = safeFile.read(buffer);

                byteBuffer.put(buffer, 0, read);
                length -= read;
            }

            byteBuffer.putLong(safeFile.readLong());// header's data length 0

            if (probe.isCancelRequested())
            {
                probe.fireCanceled();
                throw new CancellationException();
            }

            // properties
            safeFile.read(buffer, 0, ivLength);// read properties iv
            byteBuffer.put(buffer, 0, ivLength);

            IvParameterSpec iv = new IvParameterSpec(Arrays.copyOf(buffer, ivLength));
            cipher.init(Cipher.DECRYPT_MODE, encryptionKey, iv);

            length = safeFile.readLong();
            byteBuffer.putLong(length);

            while (length > 0)// read properties
            {
                if (length < buffer.length)
                    read = safeFile.read(buffer, 0, (int) length);
                else
                    read = safeFile.read(buffer, 0, buffer.length);

                length -= read;

                read = cipher.update(buffer, 0, read, bufferOut);
                byteBuffer.put(bufferOut, 0, read);

            }

            read = cipher.doFinal(bufferOut, 0);
            byteBuffer.put(bufferOut, 0, read);

            byteBuffer.putLong(safeFile.readLong());// properties data length 0

            if (probe.isCancelRequested())
            {
                probe.fireCanceled();
                throw new CancellationException();
            }

            // blocks
            while (safeFile.getFilePointer() < safeFile.length())
            {
                safeFile.read(buffer, 0, ivLength);// read properties iv
                byteBuffer.put(buffer, 0, ivLength);
                iv = new IvParameterSpec(Arrays.copyOf(buffer, ivLength));
                cipher.init(Cipher.DECRYPT_MODE, encryptionKey, iv);

                // read metadata
                length = safeFile.readLong();
                byteBuffer.putLong(length);

                while (length > 0)
                {
                    if (length < buffer.length)
                        read = safeFile.read(buffer, 0, (int) length);
                    else
                        read = safeFile.read(buffer, 0, buffer.length);

                    length -= read;

                    read = cipher.update(buffer, 0, read, bufferOut);
                    byteBuffer.put(bufferOut, 0, read);

                }

                read = cipher.doFinal(bufferOut, 0);
                byteBuffer.put(bufferOut, 0, read);

                if (probe.isCancelRequested())
                {
                    probe.fireCanceled();
                    throw new CancellationException();
                }

                // read data
                length = safeFile.readLong();
                byteBuffer.putLong(length);

                while (length > 0)
                {
                    if (length < buffer.length)
                        read = safeFile.read(buffer, 0, (int) length);
                    else
                        read = safeFile.read(buffer, 0, buffer.length);

                    length -= read;

                    read = cipher.update(buffer, 0, read, bufferOut);
                    byteBuffer.put(bufferOut, 0, read);

                    if (probe.isCancelRequested())
                    {
                        probe.fireCanceled();
                        throw new CancellationException();
                    }

                }

                read = cipher.doFinal(bufferOut, 0);
                byteBuffer.put(bufferOut, 0, read);

            }

            safeFile.seek(previousPosition);

            return HASHER.hash(byteBuffer.array());

        } catch (final CancellationException e)
        {
            throw e;
        } catch (final Exception e)
        {
            probe.fireException(e);
            throw e;
        } finally
        {
            probe.fireTerminated();
        }
    }

    /**
     * Create a new {@link Safe}
     * 
     * @param file
     * @param key
     * @param publicHeader
     * @param privateProperties
     * @param bufferSize
     * @return
     * @throws Exception
     */
    public static Safe create(final File file, final byte [] key, final Map<String, String> publicHeader, final Map<String, String> privateProperties, final int bufferSize) throws Exception
    {

        final String encryption = publicHeader.get(ENCRYPTION_LABEL);

        if (encryption == null)
            throw new Exception("Public property '" + ENCRYPTION_LABEL + "' must be set");

        if (!publicHeader.containsKey(ENCRYPTION_IV_LENGTH_LABEL))
            throw new Exception("Public property '" + ENCRYPTION_IV_LENGTH_LABEL + "' must be set");

        if (!publicHeader.containsKey(PBKDF2_SALT_LABEL))
            throw new Exception("Public property '" + PBKDF2_SALT_LABEL + "' must be set");

        if (!publicHeader.containsKey(PBKDF2_ITERATION_LABEL))
            throw new Exception("Public property '" + PBKDF2_ITERATION_LABEL + "' must be set");

        Cipher cipher = javax.crypto.Cipher.getInstance(encryption);

        final String keyAlgo = publicHeader.get(KEY_ALGO_LABEL);
        if (keyAlgo == null)
            throw new Exception("Public property '" + KEY_ALGO_LABEL + "' must be set");

        final SecretKeySpec keySpec = new SecretKeySpec(key, keyAlgo);

        if (file.exists())
            throw new IOException("File " + file + " already exist");

        if (!file.createNewFile())
            throw new IOException("Could not create file " + file);

        final RandomAccessFile raf = new RandomAccessFile(file, "rw");

        long total, position, previousPosition;

        cipher.init(Cipher.ENCRYPT_MODE, keySpec, getSecureRandom());

        raf.write(HASHER.getEmptyHash());// global hash

        // header
        position = raf.getFilePointer();
        // no IV in header
        raf.writeLong(0);
        final String header = GSON.toJson(publicHeader);
        total = write(new ByteArrayInputStream(header.getBytes(StandardCharsets.UTF_8)), raf, bufferSize, TaskProbe.DULL_PROBE);
        raf.writeLong(0);// no data in header block

        previousPosition = raf.getFilePointer();
        raf.seek(position);
        raf.writeLong(total);

        raf.seek(previousPosition);

        // properties
        position = raf.getFilePointer();
        raf.write(cipher.getIV());
        final String privatePropsJson = GSON.toJson(privateProperties == null ? new HashMap<>() : privateProperties);
        previousPosition = raf.getFilePointer();
        raf.writeLong(0L);
        total = encrypt(new ByteArrayInputStream(privatePropsJson.getBytes(StandardCharsets.UTF_8)), cipher, raf, bufferSize, TaskProbe.DULL_PROBE);
        raf.writeLong(0);// no data in properties block

        raf.seek(previousPosition);
        raf.writeLong(total);

        // write global hash
        final byte [] hash = computeHash(raf, cipher, cipher.getIV().length, keySpec, bufferSize, null);
        raf.seek(0);
        raf.write(hash);

        raf.close();

        return new Safe(file, keySpec, bufferSize);

    }

}

org/ortis/jsafebox/Utils.java

/*******************************************************************************
 * Copyright 2018 Ortis (cao.ortis.org@gmail.com)
 * 
 * Licensed 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.ortis.jsafebox;

import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.HeadlessException;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import java.util.regex.Pattern;

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

/**
 * Utility class
 * 
 * @author Ortis <br>
 *         2018 Apr 26 8:06:47 PM <br>
 */
public class Utils
{

    public final static String SEPARATOR_REGEX = "[/|" + Pattern.quote(java.io.File.separator) + "]";

    private final static String SYSTEM_PATH_DELIMITER_REGEX = Pattern.quote(File.separator) + "|" + Pattern.quote("/") + "|" + Pattern.quote("\");

    public static byte [] passwordToBytes(final char [] chars)
    {
        final CharBuffer charBuffer = CharBuffer.wrap(chars);
        final ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(charBuffer);
        final byte [] bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit());
        Arrays.fill(charBuffer.array(), 'u0000'); // clear sensitive data
        Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data
        return bytes;
    }

    /**
     * Open a {@link Safe}
     * 
     * @param safeFilePath:
     *            system path to the safe file
     * @param password:
     *            the encryption password
     * @param bufferSize:size
     *            of the <code>byte</code> buffer to be used in IO operation
     * @param log
     * @return
     * @throws Exception
     */
    public static Safe open(final String safeFilePath, final char [] password, final int bufferSize, final Logger log) throws Exception
    {
        final File file = new File(safeFilePath);

        if (!file.exists())
            throw new IOException("Safe file " + file + " doest not exist");

        final Map<String, String> header = Safe.readHeader(file, bufferSize);

        final String encyption = header.get(Safe.ENCRYPTION_LABEL);
        if (encyption == null)
            throw new Exception("Could not read property '" + Safe.ENCRYPTION_LABEL + "' from header");

        if (log != null)
            log.fine("Encryption type " + encyption);

        if (!header.containsKey(Safe.KEY_ALGO_LABEL))
            throw new Exception("Could not read property '" + Safe.KEY_ALGO_LABEL + "' from header");

        if (log != null)
            log.fine("Key algorithm " + header.get(Safe.KEY_ALGO_LABEL));


        final byte [] salt = (byte [])Safe.GSON.fromJson(header.get(Safe.PBKDF2_SALT_LABEL), Safe.BYTE_ARRAY_TYPE);

        PBEKeySpec spec = new PBEKeySpec(password, salt, Safe.PBKDF2_ITERATION, 128);
        SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
        final byte [] key = skf.generateSecret(spec).getEncoded();

        final SecretKeySpec keySpec = new SecretKeySpec(key, header.get(Safe.KEY_ALGO_LABEL));

        return new Safe(file, keySpec, bufferSize);
    }

    public static List<java.io.File> parseSystemPath(String query, final List<java.io.File> destination) throws IOException
    {
        final String [] tokens = query.split(SYSTEM_PATH_DELIMITER_REGEX);

        Path baseDirectory = null;

        if (tokens[0].equals(".") || tokens[0].equals(".."))
        {
            baseDirectory = new File(tokens[0]).toPath();

            final StringBuilder sb = new StringBuilder();
            for (int i = 1; i < tokens.length; i++)
                if (sb.length() == 0)
                    sb.append(tokens[i]);
                else
                    sb.append(File.separator + tokens[i]);

            query = "**" + File.separator + sb.toString();

        } else
        {

            final String comparableToken = tokens[0].toUpperCase();
            for (final File root : File.listRoots())
                if (root.getAbsolutePath().toUpperCase().equals(comparableToken))
                {
                    // perfect match
                    baseDirectory = root.toPath();
                    break;

                }

            if (baseDirectory == null)
                for (final File root : File.listRoots())
                {
                    String rootPath = root.getAbsolutePath().toUpperCase();
                    rootPath = rootPath.substring(0, rootPath.length() - 1);
                    if (rootPath.equals(comparableToken))
                    {
                        baseDirectory = root.toPath();
                        break;
                    }

                }
        }

        if (baseDirectory == null)
            throw new IOException("Could not locate base directory '" + tokens[0] + "'");

        Path path = baseDirectory;
        for (int i = 1; i < tokens.length; i++)
        {

            try
            {
                path = Paths.get(path.toString(), tokens[i]);
            } catch (final Exception e)
            {
                // here, we have reach a special character and the start point for the search is
                // in path
            }
        }

        final String escapedQuery = query.replace("\", "\\");// PathMatcher does not escape backslash properly. Need to do the escape manually for Windows OS path handling. This might be a bug of Java implentation.
        // Need to check on Oracle bug report database.

        final PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:" + escapedQuery);
        Files.walkFileTree(path, new FileVisitor<Path>()
        {

            @Override
            public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) throws IOException
            {

                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult preVisitDirectory(final Path dir, final BasicFileAttributes attrs) throws IOException
            {
                if (pathMatcher.matches(dir))
                {
                    destination.add(dir.toFile());
                    return FileVisitResult.SKIP_SUBTREE;
                }

                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException
            {

                if (pathMatcher.matches(file))
                    destination.add(file.toFile());

                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFileFailed(final Path file, final IOException exc) throws IOException
            {
                return FileVisitResult.CONTINUE;
            }
        });

        return destination;

    }

    /**
     * Return the MIME type of a file
     * 
     * @param file
     * @return
     */
    public static String getMIMEType(final java.io.File file)
    {

        final String name = file.getName().toUpperCase();

        if (name.endsWith(".TXT"))
            return "text/plain";
        else if (name.endsWith(".CSV"))
            return "text/csv";
        else if (name.endsWith(".HTM") || name.endsWith(".HTML"))
            return "text/html";
        else if (name.endsWith(".JPG") || name.endsWith(".JPEG"))
            return "image/jpg";
        else if (name.endsWith(".PNG"))
            return "image/png";
        else if (name.endsWith(".BM") || name.endsWith(".BMP"))
            return "image/bmp";
        else if (name.endsWith(".PDF"))
            return "application/pdf";
        else if (name.endsWith(".AVI"))
            return "video/x-msvideo";
        else if (name.endsWith(".MPEG"))
            return "video/mpeg";
        else if (name.endsWith(".MP4"))
            return "video/mp4";
        else if (name.endsWith(".MKV"))
            return "video/x-matroska";
        else if (name.endsWith(".MP3"))
            return "audio/mpeg";
        else
            return "application/octet-stream";
    }

    /**
     * Format the exception message
     * 
     * @param t
     * @return
     */
    public static String formatException(final Throwable t)
    {
        if (t == null)
            return null;

        final Throwable cause = t.getCause();
        final String msg = cause == null ? null : formatException(cause);
        return formatException(t.getClass(), msg, t.toString(), t.getStackTrace());

    }

    private static String formatException(final Class<?> exceptionClass, final String cause, final String msg, final StackTraceElement [] exceptionStack)
    {
        final StringBuilder builder = new StringBuilder();

        if (msg != null)
            builder.append(msg);

        if (exceptionStack != null)
        {
            builder.append(System.lineSeparator());
            for (int i = 0; i < exceptionStack.length; i++)
            {
                final String stackElement = exceptionStack[i].toString();

                builder.append(stackElement + System.lineSeparator());
            }
        }

        if (cause != null)
            builder.append("Caused by " + cause);

        return builder.toString();
    }

    /**
     * Remove forbidden <code>char</code> from the path and replace them with <code>substitute</code>
     * 
     * @param path:
     *            the path to sanitize
     * @param delimiter:
     *            delimiter of the path
     * @param substitute:
     *            replacement char
     * @return
     */
    public static String sanitize(final String path, final Character delimiter, final Character substitute)
    {
        final String [] tokens = path.split(Pattern.quote(Character.toString(delimiter)));

        final StringBuilder sb = new StringBuilder();
        for (int i = 0; i < tokens.length; i++)
        {
            if (i < tokens.length - 1)
                sb.append(sanitizeToken(tokens[i], substitute) + delimiter);
            else
                sb.append(sanitizeToken(tokens[i], substitute));

        }

        return sb.toString();
    }

    public static String sanitizeToken(final String token, final Character substitute)
    {

        final StringBuilder sb = new StringBuilder(token);

        final Character replacement = substitute;

        c: for (int i = 0; i < sb.length(); i++)
        {

            if (sb.charAt(i) == java.io.File.separatorChar || sb.charAt(i) == Folder.DELIMITER)
            {
                if (replacement == null)
                    sb.deleteCharAt(i--);
                else
                    sb.setCharAt(i, replacement);
                continue c;
            }

            for (final char c : Environment.getForbidenChars())
                if (sb.charAt(i) == c)
                {
                    if (replacement == null)
                        sb.deleteCharAt(i--);
                    else
                        sb.setCharAt(i, replacement);
                    continue c;
                }
        }
        return sb.toString();

    }

    public static boolean isHeadless()
    {
        if (GraphicsEnvironment.isHeadless())
            return true;

        try
        {
            GraphicsDevice [] screenDevices = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices();
            return screenDevices == null || screenDevices.length == 0;
        } catch (HeadlessException e)
        {
            return true;
        }
    }

}


Get this bounty!!!

#StackBounty: #java #spring-boot #apache-kafka #kubernetes Spring Kafka producers throwing TimeoutExceptions

Bounty: 50

Problem

I have a Kafka setup with three brokers in Kubernetes, set up according to the guide at https://github.com/Yolean/kubernetes-kafka. The following error message appears when producing messages from a Java client.

2018-06-06 11:15:44.103 ERROR 1 --- [ad | producer-1] o.s.k.support.LoggingProducerListener    : Exception thrown when sending a message with key='null' and payload='[...redacted...]':
org.apache.kafka.common.errors.TimeoutException: Expiring 1 record(s) for topicname-0: 30001 ms has passed since last append

Detailed setup

The listeners are set up to allow SSL producers/consumers from the outside world:

advertised.host.name = null
advertised.listeners = OUTSIDE://kafka-0.mydomain.com:32400,PLAINTEXT://:9092
advertised.port = null
listener.security.protocol.map = PLAINTEXT:PLAINTEXT,SSL:SSL,SASL_PLAINTEXT:SASL_PLAINTEXT,SASL_SSL:SASL_SSL,OUTSIDE:SSL
listeners = OUTSIDE://:9094,PLAINTEXT://:9092
inter.broker.listener.name = PLAINTEXT
host.name =
port.name = 9092

The OUTSIDE listeners are listening on kafka-0.mydomain.com, kafka-1.mydomain.com, etc. The plaintext listeners are listening on any IP, since they are cluster-local to Kubernetes.

The producer settings:

kafka:
  bootstrap-servers: kafka.mydomain.com:9092
  properties:
    security.protocol: SSL
   producer:
    batch-size: 100
    buffer-memory: 1048576 # 1MB
    retries: 1
    ssl:
      key-password: redacted
      keystore-location: file:/var/private/ssl/kafka.client.keystore.jks
      keystore-password: redacted
      truststore-location: file:/var/private/ssl/kafka.client.truststore.jks
      truststore-password: redacted

Analysis

  • The errors started appearing when the broker was moved moved to SSL.
  • On the server side everything is running as expected, there are no errors in the log and I can connect to the broker manually with a Kafka client tool.
  • The errors appear intermittently: sometimes it sends 30+ messages per second, sometimes it sends nothing at all. It may work like a charm for hours and then just spam timeouts for a little while.

What could it be?


Get this bounty!!!

#StackBounty: #java #ldap #spring-ldap #ldap-query Paginate on LDAP server which does not support PagedResultsControl

Bounty: 100

I’m trying to get all entries on an LDAP server using Spring LDAP (version 2.3.2). Within my code, I make use of PagedResultsDirContextProcessor to paginate through all the result. This works fine on the servers which support PagedResultsControl.

However, I now need to connect to an LDAP server which does not support PagedResultsControl. How can I get all entries without using PagedResultsControl?


Get this bounty!!!

#StackBounty: #java #spring #angular #spring-boot #spring-async Call async service which return DeferredResults, multiple times without…

Bounty: 200

my applications should have 2 core endpoints: push, pull for sending and fetching data.

Pull operation should works asynchronously and result DeferredResult. When user call pull service over rest, new DefferedResult is created and stored into Map<Long, DefferedResult> results = new ConcurrentHashMap<>() where is waiting for new data or until timeout is expired.

Push operation call user over rest as well, and this operation checks map of results for recipient of data pushed by this operation. When map contains result of recipient, these data are set to his result, DefferedResult is returned.

Here is base code:

@Service
public class FooServiceImpl {
    Map<Long, DefferedResult> results = new ConcurrentHashMap<>();

    @Override
    public DeferredResult<String> pull(Long userId) {
        DeferredResult<String> newResult = new DeferredResult<>(5000L);
        results.putIfAbsent(userId, newResult);
        newResult.onCompletion(() -> results.remove(userId));
        return newResult;
    }

    @Override
    public void push(String data, Long recipientId) {
        if (results.containsKey(recipientId)) {
            results.get(recipientId).setResult(data);
        }
    }
}

Code is working as I expected problem is that should also works for multiple users. I guess the max active users which will call pull operation will max 1000. So every call of pull take max 5 seconds as I set in DefferedResult but it isn’t.

As you can see in image, if I immediately call rest of pull operation from my javascript client multiple times you can see that tasks will executed sequentially instead of simultaneously. Tasks which I fired as last take about 25 seconds, but I need that when 1000 users execute at same time pull operation, that operation should take max 5 seconds + latency.

enter image description here

How to configure my app to execute these tasks simultaneously and ensure each each task will about 5 seconds or less (when another user send something to waiting user)? I tried add this configuration into property file:

server.tomcat.max-threads=1000

and also this configuration:

@Configuration
public class AsyncConfig extends AsyncSupportConfigurer {

    @Override
    protected AsyncTaskExecutor getTaskExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(1000);
        taskExecutor.initialize();
        return taskExecutor;
    }
}

But it didn’t help, still same result. Can you help me configure it please? Thanks in advice.

EDIT:

This is how I calling this service from angular:

this.http.get<any>(this.url, {params})
  .subscribe((data) => {
    console.log('s', data);
  }, (error) => {
    console.log('e', error);
  });

When I tried call it multiple times with pure JS code like this:

function httpGet()
{
    var xmlHttp = new XMLHttpRequest();
    xmlHttp.open( "GET", 'http://localhost:8080/api/pull?id=1', true );
    xmlHttp.send( null );
    return xmlHttp.responseText;
}
setInterval(httpGet, 500);

it will execute every request call much faster (about 7 seconds). I expected that increasing is caused database calling in service, but it still better than 25 sec. Do I have something wrong with calling this service in angular?


Get this bounty!!!

#StackBounty: #java #spring #spring-boot #logback Spring boot does not load logback-spring.xml

Bounty: 100

I have a sample Spring Boot application that uses Logback for logging.
So I have logback-spring.xml next to the jar to configure the logging, however it does not work unless I specify it with logging.config, ex : logging.config=logback-spring.xml.

I have looked into Spring Boot ignoring logback-spring.xml where it suggests that it might be because there’s already a spring.xml somewhere, but putting breakpoint on org.springframework.boot.logging.AbstractLoggingSystem.initializeWithConventions(LoggingInitializationContext, LogFile) shows that logFile is empty.

Am I doing something wrong here ?


Get this bounty!!!

#StackBounty: #java #javafx Media player component with JavaFX

Bounty: 50

I am trying to build a media player as a part of a bigger project in JavaFX. It is a simple media player as of now, which requires a Presentation component to output videos to. The dependencies are being injected by Guice.

This is my first time working on a Java project, so I would love some reviews on my code, especially based on design patterns, SOLID/DRY principles, code readability/re-usability/maintainability and how I can improve.

/**
 * Media Player component for Quizatron
 *
 * @author Dedipyaman Das <2d@twodee.me>
 * @version 1.0.18.1
 * @since 1.0.18.1
 */
public class Player {

    private MediaView mediaView;
    private MediaPlayer mediaPlayer;
    private FileChooser fileChooser;
    private Presentation presentation;
    @FXML
    private AnchorPane playerNode;
    @FXML
    private Button playBtn;
    @FXML
    private Label currTimeLbl;
    @FXML
    private Label endTimeLbl;
    @FXML
    private JFXSlider timeSlider;
    @FXML
    private Label mediaInfo;
    @FXML
    private Label sourceFileLbl;

    /**
     * Media player component constructor.
     * Guice or any other injector needs to inject a {@link FileChooser} for getting the media file,
     * {@link MediaView} for visual output
     * and {@link Presentation} to have it presented in a separate view
     *
     * @param fileChooser  FileChooser - single file
     * @param mediaView    MediaView - Parent container for visual output
     * @param presentation Presentation state for separate stage
     */
    @Inject
    public Player(FileChooser fileChooser, MediaView mediaView, Presentation presentation) {

        this.mediaView = mediaView;
        this.fileChooser = fileChooser;
        this.presentation = presentation;
    }

    public enum PlayerState {

        PLAY, PAUSE
    }

    /**
     * Initializer to get the FXML components working.
     */
    @FXML
    public void initialize() {

        timeSlider.setValue(0);
        prepareMediaView();
    }

    /**
     * Prepare the mediaview window with correct dimensions
     * Binds the mediaview's width and height relative to the window size and video ratio
     *
     * @see MediaPresentationView
     * @see Bindings
     */
    private void prepareMediaView() {

        DoubleProperty width = mediaView.fitWidthProperty();
        DoubleProperty height = mediaView.fitHeightProperty();

        width.bind(Bindings.selectDouble(mediaView.sceneProperty(), "width"));
        height.bind(Bindings.selectDouble(mediaView.sceneProperty(), "height"));
    }

    /**
     * Open the file chooser and autoplay the media
     *
     * @param event ActionEvent
     * @throws Exception thrown on failure to open file
     */
    @FXML
    private void chooseMediaFromFile(ActionEvent event) throws Exception {

        fileChooser.setTitle("Open Resource File");
        File file = fileChooser.showOpenDialog(playerNode.getScene().getWindow());

        String source = file.toURI().toURL().toExternalForm();
        Media media = new Media(source);
        startPlayer(media);
    }

    /**
     * Loads and plays the media source
     *
     * @param media Media - source media to be played
     */
    private void startPlayer(Media media) {

        loadMedia(media);
        timeSlider.setValue(this.mediaPlayer.getCurrentTime().toSeconds());
        mediaPlayer.setOnReady(this::displayMetaData);
        initTimeSlider();
        initUIControlsBehavior();
        playLoadedMedia();
    }

    /**
     * Loads the media and instantiates a new media player
     *
     * @param media Media - reusable media component, can be used from anywhere
     */
    public void loadMedia(Media media) {

        if (mediaPlayer != null) {

            mediaPlayer.dispose();
        }
        this.setMediaPlayer(new MediaPlayer(media));
    }

    /**
     * Fetches and substitutes the placeholders for media metadata
     */
    private void displayMetaData() {

        timeSlider.setMax(mediaPlayer.getTotalDuration().toSeconds());
        ObservableMap<String, Object> metaData = mediaPlayer.getMedia().getMetadata();

        String artistName = (String) metaData.get("artist");
        String title = (String) metaData.get("title");
        String album = (String) metaData.get("album");
        String mediaSource = mediaPlayer.getMedia().getSource();

        mediaInfo.setText(title + " - " + artistName + " - " + album);
        sourceFileLbl.setText(getFileNameFromPath(mediaSource));

        double duration = mediaPlayer.getTotalDuration().toSeconds();
        endTimeLbl.setText(formatTime(duration));
    }

    /**
     * Get the slider running and enable seeking
     * {@link JFXSlider}
     */
    private void initTimeSlider() {

        timeSlider
                .valueProperty()
                .addListener((observable, oldValue, newValue)
                                     -> sliderSeekBehavior(oldValue, newValue));
        mediaPlayer
                .currentTimeProperty()
                .addListener((observable, oldDuration, newDuration)
                                     -> sliderProgressBehavior(oldDuration, newDuration));
        initIndicatorValueProperty();
    }

    /**
     * The slider behavior on dragging/clicking on the JFXSlider to seek
     *
     * @param oldValue Number - before seeking
     * @param newValue Number - after action on slider
     */
    private void sliderSeekBehavior(Number oldValue, Number newValue) {

        // Is the change significant enough?
        // Drag was buggy, have to run some tests
        // Affects only the drag it seems
        double tolerance = 1;

        if (mediaPlayer.getTotalDuration().toSeconds() <= 100) {

            tolerance = 0.5;
        }
        if (abs(oldValue.doubleValue() - newValue.doubleValue()) >= tolerance) {

            mediaPlayer.seek(Duration.seconds(newValue.doubleValue()));
        }
    }

    /**
     * Behavior of the slider as it progresses
     *
     * @param oldDuration
     * @param newDuration
     */
    private void sliderProgressBehavior(Duration oldDuration, Duration newDuration) {

        // Making sure it doesn't interfere with the manual seeking
        double newElapsedTime = newDuration.toSeconds();
        double oldElapsedTime = oldDuration.toSeconds();
        double totalDuration = mediaPlayer.getTotalDuration().toSeconds();

        if (!timeSlider.isValueChanging()) {

            if (newElapsedTime - oldElapsedTime >= 0.1) {

                timeSlider.setValue(newElapsedTime);
            }
            updateTimeLabel(totalDuration, newElapsedTime);
        }
    }

    /**
     * Setting the time elapsed and time left on the appropriate indicators
     */
    private void updateTimeLabel(double totalDuration, double elapsedTime) {
        // Get rid of the unnecessary decimal points
        double timeLeft = totalDuration - elapsedTime;

        String elapsedTimeFormatted = formatTime(elapsedTime);
        String remainingTimeFormatted = formatTime(timeLeft);

        // Time elapsed/left indicators update
        currTimeLbl.setText(elapsedTimeFormatted);
        endTimeLbl.setText(remainingTimeFormatted);
    }

    /**
     * Display indicator in HH:MM format
     */
    private void initIndicatorValueProperty() {

        // Only the guy on StackOverflow and god knows how this works
        timeSlider.setValueFactory(new Callback<JFXSlider, StringBinding>() {

            @Override
            public StringBinding call(JFXSlider arg0) {

                return Bindings.createStringBinding(new java.util.concurrent.Callable<String>() {

                    @Override
                    public String call() {
                        return formatTime(timeSlider.getValue());
                    }
                }, timeSlider.valueProperty());
            }
        });
    }

    /**
     * Sets the behavior of the player UI components based on the player state
     */
    private void initUIControlsBehavior() {

        mediaPlayer.setOnEndOfMedia(this::stop);
        // Multiline lambdas should be avoided? - Venkat S.
        mediaPlayer.setOnStopped(() -> {
            // Needs to be revisited
            togglePlayPauseBtn(PlayerState.PLAY);
            timeSlider.setValue(0);
        });

        mediaPlayer.setOnPaused(() -> togglePlayPauseBtn(PlayerState.PLAY));
        mediaPlayer.setOnPlaying(() -> togglePlayPauseBtn(PlayerState.PAUSE));
    }

    /**
     * Change the pause button to a startPlayer button and have the appropriate action based on it
     *
     * @param state current state of the player
     *              {@link FontAwesomeIconView}
     */
    private void togglePlayPauseBtn(PlayerState state) {

        FontAwesomeIconView icon;

        if (state.equals(PlayerState.PLAY)) {

            icon = getIcon(FontAwesomeIcon.PLAY);
            playBtn.setOnAction(this::play);
        } else {

            icon = getIcon(FontAwesomeIcon.PAUSE);
            playBtn.setOnAction(this::pause);
        }
        playBtn.setGraphic(icon);
    }

    /**
     * Check if the media player was already there, prepare the presentation and startPlayer the video
     */
    private void playLoadedMedia() {

        if (mediaView.getMediaPlayer() != this.mediaPlayer) {

            preparePresentation();
        }
        mediaPlayer.play();
    }

    /**
     * Prepare the external presentation view
     * Get the view controller and embed visual output
     * {@link Presentation}
     */
    private void preparePresentation() {

        mediaView.setMediaPlayer(this.mediaPlayer);
        try {

            if (!(presentation.getView() instanceof MediaPresentationView)) {

                presentation.changeView("media-view");
            }

            MediaPresentationView mediaViewController = (MediaPresentationView) presentation.getView();
            mediaViewController.embedMediaView(mediaView);
        }
        catch (Exception e) {

            e.printStackTrace();
        }
    }

    /**
     * User action to startPlayer the media
     *
     * @param event ActionEvent
     */
    @FXML
    private void play(ActionEvent event) {

        mediaPlayer.play();
    }

    /**
     * User action event to pause the video
     *
     * @param event
     */
    @FXML
    private void pause(ActionEvent event) {

        mediaPlayer.pause();
    }

    /**
     * User action event to stop the media
     */
    public void stop() {

        mediaPlayer.stop();
    }

    public void openPlaylist() {

    }

    public void back() {

    }

    public void next() {

    }
    // Helper functions

    /**
     * Setter for the class media player
     *
     * @param mediaPlayer MediaPlayer {@link MediaPlayer}
     */
    private void setMediaPlayer(MediaPlayer mediaPlayer) {

        this.mediaPlayer = mediaPlayer;
    }

    /**
     * Extracts the filename + extension from the supplied file path
     *
     * @param filePath full file path
     * @return the filename stripped of slashes and everything before
     */
    private String getFileNameFromPath(String filePath) {

        filePath = filePath.replace("%20", " ");
        return filePath.substring(filePath.lastIndexOf('/') + 1, filePath.length());
    }

    /**
     * Creates a FontAwesomeIconView based on the supplied icon type
     *
     * @param iconType FontAwesome icon to be built
     * @return The final icon with appropriate styles and glyph sizes
     */
    private FontAwesomeIconView getIcon(FontAwesomeIcon iconType) {

        FontAwesomeIconView icon = new FontAwesomeIconView(iconType);
        icon.setGlyphSize(16);
        icon.setStyleClass("trackBtnIcon");
        return icon;
    }

    /**
     * Formats the time to a MM:SS format as a string
     *
     * @param totalSeconds the time specified in seconds
     * @return the formatted time string
     */
    private String formatTime(double totalSeconds) {

        int min = (int) totalSeconds / 60;
        int sec = (int) totalSeconds % 60;

        return String.format("%02d:%02d", min, sec);
    }
}

Any criticism is welcome, thanks for helping me improve my code!

Update: I have made some changes to the original code after having suggestions from people, since there are no answers yet- I would still like to have it reviewed. The purpose of the class remains the same.


Get this bounty!!!

#StackBounty: #java #xml #soap #cxf #wsdl2java CXF, wsdl2java. Custom port name

Bounty: 50

Short question:
Can I somehow bind wsdl:port name to generate custom java field name in service?

Long question:
I have a wsdl with such part:

<wsdl:service name="123xxx">
   <wsdl:port name="123xxxHttpSoap11Endpoint" binding="tns:123xxxSoap11Binding">
     <soap:address location="..."/>
   </wsdl:port>
</wsdl:service>

And a maven wsdl2java goal which generates the service with name 123xxx (which I’ve already fixed with binding by calling it MyService) and inside this service it generates a field:

public class MyService extends Service {
    ...
    public final static QName 123xxxHttpSoap11Endpoint = 
                 new QName("http://new.webservice.namespace", "123xxxHttpSoap11Endpoint");
    ...
}

This causing a compile errors because variables can’t start with numbers in Java. And I can’t find a way to somehow customize this generated code without changing the original wsdl.

So, is there a way to bind it through the cxf bindings, like it is done with service itself:

<bindings...>
    <bindings node="wsdl:definitions/wsdl:service">
    <class name="MyService"/>
    </bindings>
</bindings>

or may be there are some other ways to achive this?


Get this bounty!!!

#StackBounty: #java #sql #hibernate #jpa Hibernate CompositeUserType that is comparible in JPA-QL (or HQL) query

Bounty: 50

I’ve created a custom type for Hibernate to store an OffsetDateTime‘s timestamp and offset (because the default JPA 2.2 / Hibernate 5.2 with java 8 support implementation loses the offset information):

public class OffsetDateTimeHibernateType implements CompositeUserType {

    @Override
    public Class returnedClass() {
        return OffsetDateTime.class;
    }

    @Override
    public String[] getPropertyNames() {
        return new String[] {"dateTime", "zoneOffset"};
    }

    @Override
    public Type[] getPropertyTypes() {
        // Not sure if we should use LocalDateTimeType.INSTANCE instead of TIMESTAMP
        return new Type[]{StandardBasicTypes.TIMESTAMP, StandardBasicTypes.INTEGER};
    }

    @Override
    public Object getPropertyValue(Object o, int propertyIndex) {
        if (o == null) {
            return null;
        }
        OffsetDateTime offsetDateTime = (OffsetDateTime) o;
        switch (propertyIndex) {
            case 0:
                return Timestamp.valueOf(offsetDateTime.toLocalDateTime());
            case 1:
                return offsetDateTime.getOffset().getTotalSeconds();
            default:
                throw new IllegalArgumentException("The propertyIndex (" + propertyIndex
                        + ") must be 0 or 1.");
        }
    }

    @Override
    public OffsetDateTime nullSafeGet(ResultSet resultSet, String[] names, SessionImplementor session, Object owner)
            throws SQLException {
        if (resultSet == null) {
            return null;
        }
        Timestamp timestamp = (Timestamp) StandardBasicTypes.TIMESTAMP.nullSafeGet(resultSet, names[0], session, owner);
        if (timestamp == null) {
            throw new IllegalStateException("The timestamp (" + timestamp + ") for an "
                    + OffsetDateTime.class.getSimpleName() + "cannot be null.");
        }
        LocalDateTime localDateTime = timestamp.toLocalDateTime();
        Integer zoneOffsetSeconds = (Integer) StandardBasicTypes.INTEGER.nullSafeGet(resultSet, names[1], session, owner);
        if (zoneOffsetSeconds == null) {
            throw new IllegalStateException("The zoneOffsetSeconds (" + zoneOffsetSeconds + ") for an "
                    + OffsetDateTime.class.getSimpleName() + "cannot be null.");
        }
        return OffsetDateTime.of(localDateTime, ZoneOffset.ofTotalSeconds(zoneOffsetSeconds));
    }

    @Override
    public void nullSafeSet(PreparedStatement statement, Object value, int parameterIndex, SessionImplementor session)
            throws SQLException {
        if (value == null) {
            statement.setNull(parameterIndex, StandardBasicTypes.TIMESTAMP.sqlType());
            statement.setNull(parameterIndex, StandardBasicTypes.INTEGER.sqlType());
            return;
        }
        OffsetDateTime offsetDateTime = (OffsetDateTime) value;
        statement.setTimestamp(parameterIndex, Timestamp.valueOf(offsetDateTime.toLocalDateTime()));
        statement.setInt(parameterIndex, offsetDateTime.getOffset().getTotalSeconds());
    }

    // ************************************************************************
    // Mutable related methods
    // ************************************************************************

    @Override
    public boolean isMutable() {
        return false;
    }

    @Override
    public Object deepCopy(Object value) {
        return value; // OffsetDateTime is immutable
    }

    @Override
    public Object replace(Object original, Object target, SessionImplementor session, Object owner) {
        return original; // OffsetDateTime is immutable
    }

    @Override
    public void setPropertyValue(Object component, int property, Object value) {
        throw new UnsupportedOperationException("A OffsetDateTime is immutable.");
    }

    // ************************************************************************
    // Other methods
    // ************************************************************************

    @Override
    public boolean equals(Object a, Object b) {
        if (a == b) {
            return true;
        } else if (a == null || b == null) {
            return false;
        }
        return a.equals(b);
    }

    @Override
    public int hashCode(Object o) {
        if (o == null) {
            return 0;
        }
        return o.hashCode();
    }

    @Override
    public Serializable disassemble(Object value, SessionImplementor session) {
        return (Serializable) value;
    }

    @Override
    public Object assemble(Serializable cached, SessionImplementor session, Object owner) {
        return cached;
    }

}

Now, I want to be able to compare it, so this JPA-QL query works:

       @NamedQuery(name = "Shift.myQuery",
                   query = "select sa from Shift sa" +
                           " where sa.endDateTime >= :startDateTime" +
                           " and sa.startDateTime < :endDateTime")

on this model:

@Entity
public class Shift {

    @Type(type = "...OffsetDateTimeHibernateType")
    @Columns(columns = {@Column(name = "startDateTime"), @Column(name="startDateTimeOffset")})
    private OffsetDateTime startDateTime;
    @Type(type = "...OffsetDateTimeHibernateType")
    @Columns(columns = {@Column(name = "endDateTime"), @Column(name="endDateTimeOffset")})
    private OffsetDateTime endDateTime;

    ...

}

But that fails with:

HHH000177: Error in named query: Shift.myQuery: org.hibernate.hql.internal.ast.QuerySyntaxException: >= operator not supported on composite types. [select sa from org.optaplanner.openshift.employeerostering.shared.shift.Shift sa where sa.endDateTime >= :startDateTime and sa.startDateTime < :endDateTime]
    at org.hibernate.hql.internal.ast.QuerySyntaxException.generateQueryException(QuerySyntaxException.java:79)
    at org.hibernate.QueryException.wrapWithQueryString(QueryException.java:103)
    at org.hibernate.hql.internal.ast.QueryTranslatorImpl.doCompile(QueryTranslatorImpl.java:218)
    at org.hibernate.hql.internal.ast.QueryTranslatorImpl.compile(QueryTranslatorImpl.java:142)
    at org.hibernate.engine.query.spi.HQLQueryPlan.<init>(HQLQueryPlan.java:115)
    at org.hibernate.engine.query.spi.HQLQueryPlan.<init>(HQLQueryPlan.java:76)
    at org.hibernate.engine.query.spi.QueryPlanCache.getHQLQueryPlan(QueryPlanCache.java:150)
    at org.hibernate.internal.NamedQueryRepository.checkNamedQueries(NamedQueryRepository.java:155)
    at org.hibernate.internal.SessionFactoryImpl.checkNamedQueries(SessionFactoryImpl.java:796)
    at org.hibernate.internal.SessionFactoryImpl.<init>(SessionFactoryImpl.java:492)
    at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:422)
    at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:880)

How can I make my CustomUserType comparable?


Get this bounty!!!

#StackBounty: #java #android #c++ #video #h.264 How to play raw NAL units in Andoid exoplayer?

Bounty: 50

I know that exoplayer has support for RTSP, but I need c++ code that works on players from lots of OSs, so I need to parse the RTP packet in C++ to NAL units before passing to exoplayer

I found a way to decode RTP packets using live555 and extract its NAL units. According to ExoPlayer’s documentation:

Components common to all ExoPlayer implementations are:

A MediaSource that defines the media to be played, loads the media, and from which the loaded media can be read.

A MediaSource is
injected via ExoPlayer.prepare at the start of playback. …

So I need a custom MediaSource that can extract NAL units from my C++ code.

At the class reference for MediaSource we can see that there are already some MediaSources available. I though maybe SmoothStreaming MediaSource could work but there’s no description of what it does exactly and in its constructor I have to provide an Uri or an SsManifest (what).

I can see that there exists a NAL unit utility in this library so maybe things are already half done

So how to build or use an already available MediaSource to read NAL units for ExoPlayer to play?

As an additinal, how would you pass the NAL units from C++ to Java? In the code I found it’s simply storing them in a C++ buffer. Should I read this buffer in Java somehow?


Get this bounty!!!

#StackBounty: #java #proxy #google-apps #http-proxy Need a Java Class/tool/utility which can allow to do HTTP REST Call data manipulati…

Bounty: 50

We have very limited licences, So is it possible that I can fetch same data from Cloud applications via some utility/proxies which can do little data manipulation for us before storing it into our application server.

Consider the usecase, I have only 100 users on GoogleApps and I want to load 10K users

  1. Now, I want a man in middle i.e. proxy which can basically cheat our application server that there are N number(e.g. 10K) of users present on Cloud system.

  2. As my Application Server believes that there are still many users present on Cloud system, it will keep generating request for next chunk of user data until we don’t cross number N

  3. It will also manipulate the data that is being sent to my application server with incremental number e.g. User1, User2,…, User100, and from next chunk User101,…, User100000 etc.

I found few HTTP proxy java projects here, can anybody help me to implement above mentioned scenarios and which one I should go for from below

  1. https://github.com/adamfisk/LittleProxy
  2. https://github.com/geosolutions-it/http-proxy

  3. https://github.com/stefano-lupo/Java-Proxy-Server


Get this bounty!!!