All Downloads are FREE. Search and download functionalities are using the official Maven repository.

bt.peer.lan.AnnounceMessage Maven / Gradle / Ivy

There is a newer version: 1.10
Show newest version
/*
 * Copyright (c) 2016—2017 Andrei Tomashpolskiy and individual contributors.
 *
 * 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 bt.peer.lan;

import bt.metainfo.TorrentId;
import bt.protocol.Protocols;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.StringTokenizer;

import static bt.net.InternetProtocolUtils.getLiteralIP;

class AnnounceMessage {
    private static final Charset ascii = Charset.forName("ASCII");
    private static final String DELIMITER = "\r\n";
    private static final String HEADER = "BT-SEARCH * HTTP/1.1" + DELIMITER;
    private static final String TERMINATOR = DELIMITER + DELIMITER;

    public static int calculateMessageSize(int numberOfTorrentIds) {
        return HEADER.length()
                + 6 + 4*8+7+2 /*IPv6 literal max length*/ + 1/*colon*/ + 5/*port*/ + DELIMITER.length() /*port literal max length*/ // "Host: :\r\n"
                + 6 + 5 /*port literal max length*/ + DELIMITER.length() // "Port: \r\n"
                + (10 + 40 /*infohash length in hex*/ + DELIMITER.length()) * numberOfTorrentIds // "Infohash: \r\n"
                + 8 + (Cookie.maxLength()*2)/*pessimistic estimate for other libs*/ + DELIMITER.length() // "cookie: \r\n"
                + DELIMITER.length() * 2; // "\r\n\r\n"
    }

    public static Builder builder() {
        return new Builder();
    }

    private final Cookie cookie;
    private final int port;
    private final Set ids;

    private AnnounceMessage(Cookie cookie, int port, Set ids) {
        this.port = port;
        this.ids = ids;
        this.cookie = cookie;
    }

    public Cookie getCookie() {
        return cookie;
    }

    public int getPort() {
        return port;
    }

    public Set getTorrentIds() {
        return ids;
    }

    public void writeTo(ByteBuffer buffer, InetSocketAddress recipient) {
        byte[] message = getMessageBytes(recipient);
        if (buffer.remaining() < message.length) {
            throw new IllegalStateException("Can't write message to buffer: insufficient space");
        }
        buffer.put(message);
    }

    /*
        BT-SEARCH * HTTP/1.1\r\n
        Host: \r\n
        Port: \r\n
        Infohash: \r\n
        ...
        cookie: \r\n
        \r\n
        \r\n
     */
    private byte[] getMessageBytes(InetSocketAddress recipient) {
        StringBuilder buf = new StringBuilder();

        buf.append(HEADER);

        buf.append("Host: ");
        buf.append(getLiteralIP(recipient.getAddress()));
        buf.append(":");
        buf.append(recipient.getPort());
        buf.append("\r\n");

        buf.append("Port: ");
        buf.append(port);
        buf.append("\r\n");

        ids.forEach(id -> {
            buf.append("Infohash: ");
            buf.append(Protocols.toHex(id.getBytes()));
            buf.append("\r\n");
        });

        buf.append("cookie: ");
        buf.append(cookie.toString());
        buf.append("\r\n");

        buf.append("\r\n");
        buf.append("\r\n");

        return buf.toString().getBytes(ascii);
    }

    public static AnnounceMessage readFrom(ByteBuffer buffer) {
        byte[] message = new byte[buffer.remaining()];
        buffer.get(message);
        return parse(message);
    }

    private static AnnounceMessage parse(byte[] bytes) {
        Builder builder = new Builder();

        String s = new String(bytes, ascii);
        if (!s.startsWith(HEADER) && !s.endsWith(TERMINATOR)) {
            throw new IllegalStateException("Message has been truncated");
        }
        s = s.substring(HEADER.length(), s.length() - TERMINATOR.length());

        StringTokenizer tokenizer = new StringTokenizer(s, DELIMITER);
        String keyValueSeparator = ": "; // colon-space
        while (tokenizer.hasMoreTokens()) {
            String token = tokenizer.nextToken();
            int k = token.indexOf(keyValueSeparator);
            if (k <= 0 || k >= token.length() - keyValueSeparator.length() - 1) {
                throw new IllegalStateException("Invalid token: " + token);
            }
            String key = token.substring(0, k);
            String value = token.substring(k + keyValueSeparator.length(), token.length());
            switch (key) {
                case "Host": {
                    // ignore
                    break;
                }
                case "Port": {
                    builder.port(Integer.parseInt(value));
                    break;
                }
                case "cookie": {
                    try {
                        builder.cookie(Cookie.fromString(value));
                    } catch (Exception e) {
                        // unsupported cookie format -- not ours
                        builder.cookie(Cookie.unknownCookie());
                    }
                    break;
                }
                case "Infohash": {
                    builder.torrentId(TorrentId.fromBytes(Protocols.fromHex(value)));
                    break;
                }
                default: {
                    // ignore
                }
            }
        }

        return builder.build();
    }

    @Override
    public String toString() {
        return "AnnounceMessage{" +
                "cookie=" + cookie +
                ", port=" + port +
                ", ids=" + ids +
                '}';
    }

    static class Builder {
        private Cookie cookie;
        private int port;
        private Set ids;

        private Builder() {
        }

        public Builder cookie(Cookie cookie) {
            if (this.cookie != null) {
                throw new IllegalStateException("Cookie already set");
            }
            this.cookie = Objects.requireNonNull(cookie);
            return this;
        }

        public Builder port(int port) {
            if (this.port > 0) {
                throw new IllegalStateException("Port already set");
            }
            if (port <= 0 || port > 65535) {
                throw new IllegalArgumentException("Invalid port number: " + port);
            }
            this.port = port;
            return this;
        }

        public Builder torrentId(TorrentId id) {
            Objects.requireNonNull(id);
            if (ids == null) {
                ids = new HashSet<>();
            }
            ids.add(id);
            return this;
        }

        AnnounceMessage build() {
            if (cookie == null) {
                throw new IllegalStateException("Can't build message: missing cookie");
            }
            if (ids == null) {
                throw new IllegalStateException("Can't build message: no torrents");
            }
            if (port == 0) {
                throw new IllegalStateException("Can't build message: missing port");
            }
            return new AnnounceMessage(cookie, port, ids);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy