├── .github
├── dependabot.yml
└── workflows
│ └── maven.yml
├── .gitignore
├── .maven.xml
├── LICENSE
├── README.md
├── pom.xml
└── src
└── main
└── java
└── xyz
└── gianlu
└── zeroconf
├── ABLock.java
├── DiscoveredService.java
├── Main.java
├── Packet.java
├── PacketListener.java
├── Record.java
├── RecordA.java
├── RecordAAAA.java
├── RecordANY.java
├── RecordPTR.java
├── RecordSRV.java
├── RecordTXT.java
├── Service.java
└── Zeroconf.java
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: maven
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | time: "04:00"
8 | open-pull-requests-limit: 10
9 |
--------------------------------------------------------------------------------
/.github/workflows/maven.yml:
--------------------------------------------------------------------------------
1 | name: Maven build / deploy
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | tags:
7 | - 'v*'
8 | pull_request:
9 | branches: [ master ]
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Set up JDK 1.8
17 | uses: actions/setup-java@v2
18 | with:
19 | java-version: '8'
20 | distribution: 'adopt'
21 | - name: Import GPG secrets
22 | if: ${{ !startsWith(github.ref, 'refs/pull') }}
23 | run: |
24 | echo $GPG_SECRET_KEYS | base64 --decode > gpg-private-key.txt
25 | gpg --pinentry-mode loopback --import --batch gpg-private-key.txt
26 | echo $GPG_OWNERTRUST | base64 --decode > gpg-ownertrust.txt
27 | gpg --pinentry-mode loopback --import-ownertrust --batch gpg-ownertrust.txt
28 | env:
29 | GPG_SECRET_KEYS: ${{ secrets.GPG_SECRET_KEYS }}
30 | GPG_OWNERTRUST: ${{ secrets.GPG_OWNERTRUST }}
31 | - name: Cache local Maven repository
32 | uses: actions/cache@v2
33 | with:
34 | path: ~/.m2/repository
35 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
36 | restore-keys: |
37 | ${{ runner.os }}-maven-
38 | - name: Build with Maven
39 | if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
40 | run: mvn clean test -Pdebug -B -U -Dgpg.skip -Dmaven.javadoc.skip=true
41 | - name: Deploy with Maven to OSSRH
42 | if: ${{ !startsWith(github.ref, 'refs/pull') }}
43 | run: mvn deploy --settings .maven.xml -B -U -Possrh
44 | env:
45 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
46 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
47 | GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
48 | - name: Deploy with Maven to GitHub packages
49 | if: ${{ startsWith(github.ref, 'refs/tags/v') }}
50 | run: mvn deploy --settings .maven.xml -B -U -Pgithub
51 | env:
52 | GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
54 | - name: Create release
55 | if: ${{ startsWith(github.ref, 'refs/tags/v') }}
56 | uses: softprops/action-gh-release@v1
57 | env:
58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
59 | with:
60 | name: ${{ github.ref }}
61 | draft: true
62 | prerelease: false
63 | files: target/zeroconf-java.jar
64 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | *.iml
3 | /target
--------------------------------------------------------------------------------
/.maven.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 | ossrh
7 | ${env.SONATYPE_USERNAME}
8 | ${env.SONATYPE_PASSWORD}
9 |
10 |
11 |
12 | github
13 | devgianlu
14 | ${env.GITHUB_TOKEN}
15 |
16 |
17 |
18 |
19 |
20 | ossrh
21 |
22 | gpg
23 | ${env.GPG_PASSPHRASE}
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 devgianlu
4 | Copyright (c) 2016 faceless2
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # zeroconf-java
2 |
3 | [](https://github.com/devgianlu/zeroconf-java/actions/workflows/maven.yml)
4 | [](https://www.codacy.com/manual/devgianlu/zeroconf-java?utm_source=github.com&utm_medium=referral&utm_content=devgianlu/zeroconf-java&utm_campaign=Badge_Grade)
5 | [](https://wakatime.com/badge/github/devgianlu/zeroconf-java)
6 | [](https://gianlu.xyz/donate/)
7 |
8 | A pure Java implementation of the Zeroconf technology. This library reuses a lot of code from [faceless2/cu-zeroconf](https://github.com/faceless2/cu-zeroconf).
9 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
3 | 4.0.0
4 |
5 | xyz.gianlu.zeroconf
6 | zeroconf
7 | 1.3.3-SNAPSHOT
8 |
9 | zeroconf-java
10 | A pure Java implementation of the Zeroconf technology
11 |
12 | https://github.com/devgianlu/zeroconf-java
13 |
14 |
15 |
16 | MIT License
17 | https://opensource.org/licenses/MIT
18 |
19 |
20 |
21 |
22 |
23 | Gianluca Altomani
24 | altomanigianluca@gmail.com
25 | https://gianlu.xyz
26 |
27 |
28 | Mike Bremford
29 | https://github.com/faceless2/
30 |
31 |
32 |
33 |
34 | scm:git:git://github.com/devgianlu/zeroconf-java.git
35 | scm:git:ssh://github.com:devgianlu/zeroconf-java.git
36 | https://github.com/devgianlu/zeroconf-java/tree/master
37 |
38 |
39 |
40 | UTF-8
41 | 1.8
42 | 1.8
43 |
44 |
45 |
46 | zeroconf-java
47 |
48 |
49 |
50 |
51 |
52 | org.jetbrains
53 | annotations
54 | 21.0.1
55 |
56 |
57 |
58 |
59 | org.slf4j
60 | slf4j-api
61 | 1.7.30
62 |
63 |
64 |
65 |
66 |
67 | ossrh
68 | https://oss.sonatype.org/content/repositories/snapshots
69 |
70 |
71 | ossrh
72 | https://oss.sonatype.org/service/local/staging/deploy/maven2
73 |
74 |
75 |
76 |
77 |
78 | debug
79 |
80 | true
81 |
82 | debug
83 |
84 |
85 |
86 |
87 | ossrh
88 |
89 |
90 | ossrh
91 |
92 |
93 |
94 |
95 |
96 |
97 | org.apache.maven.plugins
98 | maven-source-plugin
99 | 3.2.1
100 |
101 |
102 | attach-sources
103 |
104 | jar-no-fork
105 |
106 |
107 |
108 |
109 |
110 | org.apache.maven.plugins
111 | maven-javadoc-plugin
112 | 3.3.0
113 |
114 |
115 | attach-javadocs
116 |
117 | jar
118 |
119 |
120 |
121 |
122 | true
123 | false
124 |
125 |
126 |
127 |
128 |
129 | org.sonatype.plugins
130 | nexus-staging-maven-plugin
131 | 1.6.8
132 | true
133 |
134 | ossrh
135 | https://oss.sonatype.org/
136 |
137 |
138 |
139 |
140 |
141 | org.apache.maven.plugins
142 | maven-gpg-plugin
143 | 3.0.1
144 |
145 |
146 | sign-artifacts
147 | verify
148 |
149 | sign
150 |
151 |
152 |
153 |
154 | --pinentry-mode
155 | loopback
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 | github
166 |
167 |
168 | github
169 |
170 |
171 |
172 |
173 |
174 | github
175 | https://maven.pkg.github.com/devgianlu/zeroconf-java
176 |
177 |
178 |
179 |
180 |
--------------------------------------------------------------------------------
/src/main/java/xyz/gianlu/zeroconf/ABLock.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright 2021 erhannis.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | */
24 | package xyz.gianlu.zeroconf;
25 |
26 | /**
27 | * Multistage lock customized for NIO Selectors.
28 | * Extended rationale:
29 | * See, NIO Selectors take locks on their things when they select(), and don't
30 | * release them while waiting. If a select happens before any channels are
31 | * registered, the select never completes. If a register first calls wakeup
32 | * to abort the hung select, there's a small chance the select could loop back
33 | * around again before the register actually got called. Adding a plain
34 | * synchronization around the two calls doesn't fix it, because if the wakeup
35 | * is inside the sync it won't get called and the select won't return, and if
36 | * it's outside then the loop-around could still happen and nothing's been
37 | * fixed.
38 | *
39 | * So, here's my solution. Consider the following code.
40 | *
41 | * private final ABLock selectorLock = new ABLock();
42 | *
43 | * ...
44 | *
45 | * // Channel registration
46 | * selectorLock.lockA1();
47 | * try {
48 | * getSelector().wakeup();
49 | * selectorLock.lockA2();
50 | * try {
51 | * channels.put(nic, channel.register(getSelector(), SelectionKey.OP_READ));
52 | * } finally {
53 | * selectorLock.unlockA2();
54 | * }
55 | * } finally {
56 | * selectorLock.unlockA1();
57 | * }
58 | *
59 | * ...
60 | *
61 | * // Select
62 | * Selector selector = getSelector();
63 | * selectorLock.lockB();
64 | * try {
65 | * selector.select();
66 | * } finally {
67 | * selectorLock.unlockB();
68 | * }
69 | *
70 | *
71 | * The goal is to prevent reentering `select` between a `wakeup` and `register`.
72 | * To that end:
73 | * B blocks A2.
74 | * A1 blocks B.
75 | * A2 blocks B.
76 | * Nothing else blocks anything else.
77 | * By e.g. "A2 blocks B", I mean "if a lock exists on A2, attempts to acquire a
78 | * lock on B will block until there are no more locks on A2." So, in the
79 | * registration thread, you acquire A1 to prevent any new B locks, call wakeup
80 | * to get the Selector to drop its B lock, then lock on A2 to wait for it to do
81 | * so, then make your registration, then drop the locks (in reverse order, as is
82 | * custom). On the select thread, you acquire B, then select, then release B.
83 | * Unless I've made a goof:
84 | * You can have multiple registrations occur at once, and multiple selections
85 | * occur at once, but never both registrations and selections, and a
86 | * (lock for a) registration will not wait forever for a selection to conclude.
87 | * HOWEVER.
88 | * I've only tested this briefly. I'd want to test it a lot more before I was
89 | * very comfortable with it, and I don't really have time right now. I suspect
90 | * it's better than nothing, at least, though.
91 | *
92 | * @author erhannis
93 | */
94 | public final class ABLock {
95 | private final Object sync = new Object();
96 | private long a1 = 0;
97 | private long a2 = 0;
98 | private long b = 0;
99 |
100 | /**
101 | * For every call to lockA1, there must be exactly one subsequent call to unlockA1.
102 | * Attempted locks on A1 are not blocked by anything.
103 | * An existing lock on A1 blocks new locks on B.
104 | * An existing lock on A1 DOES NOT block new locks on A1 or A2.
105 | */
106 | public void lockA1() {
107 | synchronized (sync) {
108 | a1++;
109 | sync.notifyAll();
110 | }
111 | }
112 |
113 | public void unlockA1() {
114 | synchronized (sync) {
115 | a1--;
116 | sync.notifyAll();
117 | }
118 | }
119 |
120 | /**
121 | * For every call to lockA2, there must be a subsequent call to unlockA2.
122 | * Attempted locks on A2 are blocked by existing locks on B, and nothing else.
123 | * An existing lock on A2 blocks new locks on B.
124 | * An existing lock on A2 DOES NOT block new locks on A1 or A2.
125 | */
126 | public void lockA2() throws InterruptedException {
127 | synchronized (sync) {
128 | while (b > 0)
129 | sync.wait();
130 |
131 | a2++;
132 | sync.notifyAll();
133 | }
134 | }
135 |
136 | public void unlockA2() {
137 | synchronized (sync) {
138 | a2--;
139 | sync.notifyAll();
140 | }
141 | }
142 |
143 | /**
144 | * For every call to lockB, there must be a subsequent call to unlockB.
145 | * Attempted locks on B are blocked by existing locks on A1 and A2, and nothing else.
146 | * An existing lock on B blocks new locks on A2.
147 | * An existing lock on B DOES NOT block new locks on A1 or B.
148 | */
149 | public void lockB() throws InterruptedException {
150 | synchronized (sync) {
151 | while (a1 > 0 || a2 > 0)
152 | sync.wait();
153 |
154 | b++;
155 | sync.notifyAll();
156 | }
157 | }
158 |
159 | public void unlockB() {
160 | synchronized (sync) {
161 | b--;
162 | sync.notifyAll();
163 | }
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/src/main/java/xyz/gianlu/zeroconf/DiscoveredService.java:
--------------------------------------------------------------------------------
1 | package xyz.gianlu.zeroconf;
2 |
3 | import org.jetbrains.annotations.NotNull;
4 |
5 | import java.net.InetSocketAddress;
6 | import java.util.ArrayList;
7 | import java.util.List;
8 |
9 | /**
10 | * @author devgianlu
11 | */
12 | public final class DiscoveredService {
13 | public final String target;
14 | public final int port;
15 | public final String name;
16 | public final String service;
17 | public final String protocol;
18 | public final String domain;
19 | public final String serviceName;
20 | private final long expiration;
21 | private final List relatedRecords = new ArrayList<>(5);
22 |
23 | DiscoveredService(@NotNull RecordSRV record) {
24 | expiration = System.currentTimeMillis() + record.ttl * 1000L;
25 |
26 | target = record.getTarget();
27 | port = record.getPort();
28 | serviceName = record.getName();
29 |
30 | String[] split = serviceName.split("\\.");
31 | if (split.length != 4) throw new IllegalArgumentException("Invalid service name: " + record.getName());
32 |
33 | name = split[0];
34 | service = split[1];
35 | protocol = split[2];
36 | domain = "." + split[3];
37 | }
38 |
39 | public boolean isExpired() {
40 | return System.currentTimeMillis() > expiration;
41 | }
42 |
43 | void addRelatedRecord(@NotNull Record record) {
44 | relatedRecords.removeIf(Record::isExpired);
45 | relatedRecords.add(record);
46 | }
47 |
48 | @NotNull
49 | public List getRelatedRecords() {
50 | return new ArrayList<>(relatedRecords);
51 | }
52 |
53 | @NotNull
54 | public List getAddresses() {
55 | List list = new ArrayList<>(3);
56 | for (Record record : relatedRecords) {
57 | if (record instanceof RecordSRV)
58 | list.add(new InetSocketAddress(((RecordSRV) record).getTarget(), ((RecordSRV) record).getPort()));
59 | }
60 | return list;
61 | }
62 |
63 | @Override
64 | public boolean equals(Object o) {
65 | if (this == o) return true;
66 | if (o == null || getClass() != o.getClass()) return false;
67 | DiscoveredService that = (DiscoveredService) o;
68 | if (port != that.port) return false;
69 | if (!target.equals(that.target)) return false;
70 | if (!name.equals(that.name)) return false;
71 | if (!service.equals(that.service)) return false;
72 | if (!protocol.equals(that.protocol)) return false;
73 | return domain.equals(that.domain);
74 | }
75 |
76 | @Override
77 | public int hashCode() {
78 | int result = target.hashCode();
79 | result = 31 * result + port;
80 | result = 31 * result + name.hashCode();
81 | result = 31 * result + service.hashCode();
82 | result = 31 * result + protocol.hashCode();
83 | result = 31 * result + domain.hashCode();
84 | return result;
85 | }
86 |
87 | @Override
88 | public String toString() {
89 | return "DiscoveredService{" +
90 | "target='" + target + '\'' +
91 | ", port=" + port +
92 | ", name='" + name + '\'' +
93 | ", service='" + service + '\'' +
94 | ", protocol='" + protocol + '\'' +
95 | ", domain='" + domain + '\'' +
96 | '}';
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/main/java/xyz/gianlu/zeroconf/Main.java:
--------------------------------------------------------------------------------
1 | package xyz.gianlu.zeroconf;
2 |
3 | import java.io.IOException;
4 |
5 | /**
6 | * @author Gianlu
7 | */
8 | public class Main {
9 | public static void main(String[] args) throws IOException {
10 | Zeroconf zeroconf = new Zeroconf();
11 | zeroconf.setUseIpv4(true)
12 | .setUseIpv6(false)
13 | .addAllNetworkInterfaces();
14 |
15 | Runtime.getRuntime().addShutdownHook(new Thread(zeroconf::close));
16 |
17 | // Announce service
18 | Service service = new Service(args[0], args[1], Integer.parseInt(args[2]));
19 | zeroconf.announce(service);
20 |
21 | // Start discovering
22 | Zeroconf.DiscoveredServices services = zeroconf.discover(args[1], "tcp", ".local");
23 | while (true) {
24 | System.out.println(services.getServices());
25 |
26 | try {
27 | Thread.sleep(1000);
28 | } catch (InterruptedException ex) {
29 | break;
30 | }
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/java/xyz/gianlu/zeroconf/Packet.java:
--------------------------------------------------------------------------------
1 | package xyz.gianlu.zeroconf;
2 |
3 | import java.net.InetSocketAddress;
4 | import java.nio.ByteBuffer;
5 | import java.util.ArrayList;
6 | import java.util.List;
7 |
8 | /**
9 | * A Service Discovery Packet. This class is only of interest to developers.
10 | */
11 | public final class Packet {
12 | private static final int FLAG_RESPONSE = 15;
13 | private static final int FLAG_AA = 10;
14 | private final List questions;
15 | private final List answers;
16 | private final List authorities;
17 | private final List additionals;
18 | private int id;
19 | private int flags;
20 | private InetSocketAddress address;
21 |
22 | Packet() {
23 | this(0);
24 | }
25 |
26 | Packet(int id) {
27 | this.id = id;
28 | questions = new ArrayList<>();
29 | answers = new ArrayList<>();
30 | authorities = new ArrayList<>();
31 | additionals = new ArrayList<>();
32 | setResponse(true);
33 | }
34 |
35 | InetSocketAddress getAddress() {
36 | return address;
37 | }
38 |
39 | void setAddress(InetSocketAddress address) {
40 | this.address = address;
41 | }
42 |
43 | int getID() {
44 | return id;
45 | }
46 |
47 | /**
48 | * Return true if it's a response, false if it's a query
49 | */
50 | boolean isResponse() {
51 | return isFlag(FLAG_RESPONSE);
52 | }
53 |
54 | void setResponse(boolean on) {
55 | setFlag(FLAG_RESPONSE, on);
56 | }
57 |
58 | boolean isAuthoritative() {
59 | return isFlag(FLAG_AA);
60 | }
61 |
62 | void setAuthoritative(boolean on) {
63 | setFlag(FLAG_AA, on);
64 | }
65 |
66 | private boolean isFlag(int flag) {
67 | return (flags & (1 << flag)) != 0;
68 | }
69 |
70 | private void setFlag(int flag, boolean on) {
71 | if (on) flags |= (1 << flag);
72 | else flags &= ~(1 << flag);
73 | }
74 |
75 | void read(ByteBuffer in, InetSocketAddress address) {
76 | byte[] q = new byte[in.remaining()];
77 | in.get(q);
78 | in.position(0);
79 |
80 | this.address = address;
81 | id = in.getShort() & 0xFFFF;
82 | flags = in.getShort() & 0xFFFF;
83 | int numquestions = in.getShort() & 0xFFFF;
84 | int numanswers = in.getShort() & 0xFFFF;
85 | int numauthorities = in.getShort() & 0xFFFF;
86 | int numadditionals = in.getShort() & 0xFFFF;
87 |
88 | for (int i = 0; i < numquestions; i++) {
89 | questions.add(Record.readQuestion(in));
90 | }
91 |
92 | for (int i = 0; i < numanswers; i++) {
93 | if (in.hasRemaining()) answers.add(Record.readAnswer(in));
94 | }
95 |
96 | for (int i = 0; i < numauthorities; i++) {
97 | if (in.hasRemaining()) authorities.add(Record.readAnswer(in));
98 | }
99 |
100 | for (int i = 0; i < numadditionals; i++) {
101 | if (in.hasRemaining()) additionals.add(Record.readAnswer(in));
102 | }
103 | }
104 |
105 | void write(ByteBuffer out) {
106 | out.putShort((short) id);
107 | out.putShort((short) flags);
108 | out.putShort((short) questions.size());
109 | out.putShort((short) answers.size());
110 | out.putShort((short) authorities.size());
111 | out.putShort((short) additionals.size());
112 | for (Record r : questions) r.write(out, this);
113 | for (Record r : answers) r.write(out, this);
114 | for (Record r : authorities) r.write(out, this);
115 | for (Record r : additionals) r.write(out, this);
116 | }
117 |
118 | @Override
119 | public String toString() {
120 | return "Packet{" +
121 | "id=" + id +
122 | ", flags=" + flags +
123 | ", questions=" + questions +
124 | ", answers=" + answers +
125 | ", authorities=" + authorities +
126 | ", additionals=" + additionals +
127 | ", address=" + address +
128 | '}';
129 | }
130 |
131 | List getQuestions() {
132 | return questions;
133 | }
134 |
135 | List getAnswers() {
136 | return answers;
137 | }
138 |
139 | List getAdditionals() {
140 | return additionals;
141 | }
142 |
143 | void addAnswer(Record record) {
144 | answers.add(record);
145 | }
146 |
147 | void addQuestion(Record record) {
148 | questions.add(record);
149 | }
150 |
151 | void addAdditional(Record record) {
152 | additionals.add(record);
153 | }
154 |
155 | void addAuthority(Record record) {
156 | authorities.add(record);
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/src/main/java/xyz/gianlu/zeroconf/PacketListener.java:
--------------------------------------------------------------------------------
1 | package xyz.gianlu.zeroconf;
2 |
3 | import org.jetbrains.annotations.NotNull;
4 |
5 | /**
6 | * An interface that will be notified of a packet transmission
7 | *
8 | * @see Zeroconf#addReceiveListener
9 | * @see Zeroconf#addSendListener
10 | */
11 | public interface PacketListener {
12 | void packetEvent(@NotNull Packet packet);
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/xyz/gianlu/zeroconf/Record.java:
--------------------------------------------------------------------------------
1 | package xyz.gianlu.zeroconf;
2 |
3 | import java.nio.ByteBuffer;
4 | import java.util.Arrays;
5 |
6 | /**
7 | * Base class for a DNS record. Written entirely without the benefit of specifications.
8 | */
9 | public class Record {
10 | static final int TYPE_A = 0x01;
11 | static final int TYPE_PTR = 0x0C;
12 | static final int TYPE_CNAME = 0x05;
13 | static final int TYPE_TXT = 0x10;
14 | static final int TYPE_AAAA = 0x1C;
15 | static final int TYPE_SRV = 0x21;
16 | static final int TYPE_NSEC = 0x2F;
17 | static final int TYPE_ANY = 0xFF;
18 | private final long timestamp;
19 | private final int type;
20 | protected int ttl;
21 | private String name;
22 | private int clazz;
23 | private byte[] data;
24 |
25 | Record(int type) {
26 | this.timestamp = System.currentTimeMillis();
27 | this.type = type & 0xFFFF;
28 | setTTL(4500);
29 | this.clazz = 1;
30 | }
31 |
32 | protected static int writeName(String name, ByteBuffer out, Packet packet) {
33 | int len = name.length();
34 | int start = 0;
35 | for (int i = 0; i <= len; i++) {
36 | char c = i == len ? '.' : name.charAt(i);
37 | if (c == '.') {
38 | out.put((byte) (i - start));
39 | for (int j = start; j < i; j++)
40 | out.put((byte) name.charAt(j));
41 |
42 | start = i + 1;
43 | }
44 | }
45 |
46 | out.put((byte) 0);
47 | return name.length() + 2;
48 | }
49 |
50 | protected static String readName(ByteBuffer in) {
51 | StringBuilder sb = new StringBuilder();
52 | int len;
53 | while ((len = (in.get() & 0xFF)) > 0) {
54 | if (len >= 0x40) {
55 | int off = ((len & 0x3F) << 8) | (in.get() & 0xFF); // Offset from start of packet
56 | int oldPos = in.position();
57 | in.position(off);
58 | if (sb.length() > 0) sb.append('.');
59 | sb.append(readName(in));
60 | in.position(oldPos);
61 | break;
62 | } else {
63 | if (sb.length() > 0) sb.append('.');
64 | while (len-- > 0) sb.append((char) (in.get() & 0xFF));
65 | }
66 | }
67 | return sb.toString();
68 | }
69 |
70 | static Record readAnswer(ByteBuffer in) {
71 | String name = readName(in);
72 | int type = in.getShort() & 0xFFFF;
73 | Record record = getInstance(type);
74 | record.setName(name);
75 | record.clazz = in.getShort() & 0xFFFF;
76 | record.ttl = in.getInt();
77 | int len = in.getShort() & 0xFFFF;
78 | record.readData(len, in);
79 | return record;
80 | }
81 |
82 | static Record readQuestion(ByteBuffer in) {
83 | String name = readName(in);
84 | int type = in.getShort() & 0xFFFF;
85 | Record record = getInstance(type);
86 | record.setName(name);
87 | record.clazz = in.getShort() & 0xFFFF;
88 | return record;
89 | }
90 |
91 | private static Record getInstance(int type) {
92 | switch (type) {
93 | case TYPE_A:
94 | return new RecordA();
95 | case TYPE_AAAA:
96 | return new RecordAAAA();
97 | case TYPE_SRV:
98 | return new RecordSRV();
99 | case TYPE_PTR:
100 | return new RecordPTR();
101 | case TYPE_TXT:
102 | return new RecordTXT();
103 | case TYPE_ANY:
104 | return new RecordANY();
105 | default:
106 | return new Record(type);
107 | }
108 | }
109 |
110 | public String getName() {
111 | return name;
112 | }
113 |
114 | Record setName(String name) {
115 | this.name = name;
116 | return this;
117 | }
118 |
119 | public int getType() {
120 | return type;
121 | }
122 |
123 | Record setTTL(int ttl) {
124 | this.ttl = ttl;
125 | return this;
126 | }
127 |
128 | protected void readData(int len, ByteBuffer in) {
129 | data = new byte[len];
130 | in.get(data);
131 | }
132 |
133 | protected int writeData(ByteBuffer out, Packet packet) {
134 | if (data != null) {
135 | out.put(data);
136 | return data.length;
137 | } else {
138 | return -1;
139 | }
140 | }
141 |
142 | public boolean isUnicastQuery() {
143 | return (clazz & 0x80) != 0;
144 | }
145 |
146 | public boolean isExpired() {
147 | return System.currentTimeMillis() > timestamp + ttl * 1000L;
148 | }
149 |
150 | @Override
151 | public String toString() {
152 | return "Record{" +
153 | "type=" + type +
154 | ", ttl=" + ttl +
155 | ", name='" + name + '\'' +
156 | ", clazz=" + clazz +
157 | ", data=" + Arrays.toString(data) +
158 | '}';
159 | }
160 |
161 | void write(ByteBuffer out, Packet packet) {
162 | writeName(name, out, packet);
163 | out.putShort((short) type);
164 | out.putShort((short) clazz);
165 | out.putInt(ttl);
166 | int pos = out.position();
167 | out.putShort((short) 0);
168 | int len = writeData(out, packet);
169 | if (len > 0) out.putShort(pos, (short) len);
170 | else out.position(pos);
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/src/main/java/xyz/gianlu/zeroconf/RecordA.java:
--------------------------------------------------------------------------------
1 | package xyz.gianlu.zeroconf;
2 |
3 | import org.jetbrains.annotations.NotNull;
4 | import org.jetbrains.annotations.Nullable;
5 |
6 | import java.io.IOException;
7 | import java.net.Inet4Address;
8 | import java.net.InetAddress;
9 | import java.nio.ByteBuffer;
10 |
11 | public final class RecordA extends Record {
12 | private byte[] address;
13 |
14 | RecordA() {
15 | super(TYPE_A);
16 | }
17 |
18 | RecordA(String name, Inet4Address address) {
19 | this();
20 | setName(name);
21 | setTTL(120);
22 | this.address = address.getAddress();
23 | }
24 |
25 | @Override
26 | protected void readData(int len, ByteBuffer in) {
27 | address = new byte[len];
28 | in.get(address);
29 | }
30 |
31 | @Override
32 | protected int writeData(ByteBuffer out, Packet packet) {
33 | if (address != null) {
34 | out.put(address);
35 | return address.length;
36 | } else {
37 | return -1;
38 | }
39 | }
40 |
41 | @Nullable
42 | public Inet4Address getAddress() {
43 | try {
44 | return address == null ? null : (Inet4Address) InetAddress.getByAddress(address);
45 | } catch (IOException ex) {
46 | throw new IllegalStateException(ex);
47 | }
48 | }
49 |
50 | @Override
51 | @NotNull
52 | public String toString() {
53 | return "{type:a, name:\"" + getName() + "\", address:\"" + getAddress() + "\"}";
54 | }
55 | }
56 |
57 |
--------------------------------------------------------------------------------
/src/main/java/xyz/gianlu/zeroconf/RecordAAAA.java:
--------------------------------------------------------------------------------
1 | package xyz.gianlu.zeroconf;
2 |
3 | import org.jetbrains.annotations.NotNull;
4 | import org.jetbrains.annotations.Nullable;
5 |
6 | import java.io.IOException;
7 | import java.net.Inet6Address;
8 | import java.net.InetAddress;
9 | import java.nio.ByteBuffer;
10 |
11 | public final class RecordAAAA extends Record {
12 | private byte[] address;
13 |
14 | RecordAAAA() {
15 | super(TYPE_AAAA);
16 | }
17 |
18 | RecordAAAA(String name, Inet6Address value) {
19 | this();
20 | setName(name);
21 | setTTL(120);
22 | this.address = value.getAddress();
23 | }
24 |
25 | @Override
26 | protected void readData(int len, ByteBuffer in) {
27 | address = new byte[len];
28 | in.get(address);
29 | }
30 |
31 | @Override
32 | protected int writeData(ByteBuffer out, Packet packet) {
33 | if (address != null) {
34 | out.put(address);
35 | return address.length;
36 | } else {
37 | return -1;
38 | }
39 | }
40 |
41 | @Nullable
42 | public Inet6Address getAddress() {
43 | try {
44 | return address == null ? null : (Inet6Address) InetAddress.getByAddress(address);
45 | } catch (IOException ex) {
46 | throw new IllegalStateException(ex);
47 | }
48 | }
49 |
50 | @Override
51 | @NotNull
52 | public String toString() {
53 | return "{type:aaaa, name:\"" + getName() + "\", address:\"" + getAddress() + "\"}";
54 | }
55 | }
56 |
57 |
--------------------------------------------------------------------------------
/src/main/java/xyz/gianlu/zeroconf/RecordANY.java:
--------------------------------------------------------------------------------
1 | package xyz.gianlu.zeroconf;
2 |
3 | import org.jetbrains.annotations.NotNull;
4 |
5 | import java.nio.ByteBuffer;
6 |
7 | public final class RecordANY extends Record {
8 |
9 | RecordANY() {
10 | super(TYPE_ANY);
11 | }
12 |
13 | RecordANY(String name) {
14 | this();
15 | setName(name);
16 | }
17 |
18 | @Override
19 | protected void readData(int len, ByteBuffer in) {
20 | throw new IllegalStateException();
21 | }
22 |
23 | @Override
24 | protected int writeData(ByteBuffer out, Packet packet) {
25 | return -1;
26 | }
27 |
28 | @Override
29 | @NotNull
30 | public String toString() {
31 | return "{type:any, name:\"" + getName() + "\"}";
32 | }
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/src/main/java/xyz/gianlu/zeroconf/RecordPTR.java:
--------------------------------------------------------------------------------
1 | package xyz.gianlu.zeroconf;
2 |
3 | import org.jetbrains.annotations.NotNull;
4 |
5 | import java.nio.ByteBuffer;
6 |
7 | public final class RecordPTR extends Record {
8 | private String value;
9 |
10 | RecordPTR() {
11 | super(TYPE_PTR);
12 | }
13 |
14 | RecordPTR(String name, String value) {
15 | this();
16 | setName(name);
17 | this.value = value;
18 | }
19 |
20 | /**
21 | * For queries
22 | */
23 | RecordPTR(String name) {
24 | this();
25 | setName(name);
26 | }
27 |
28 | @Override
29 | protected void readData(int len, ByteBuffer in) {
30 | value = readName(in);
31 | }
32 |
33 | @Override
34 | protected int writeData(ByteBuffer out, Packet packet) {
35 | return value != null ? writeName(value, out, packet) : -1;
36 | }
37 |
38 | public String getValue() {
39 | return value;
40 | }
41 |
42 | @Override
43 | @NotNull
44 | public String toString() {
45 | StringBuilder sb = new StringBuilder();
46 | sb.append("{type:ptr, name:\"");
47 | sb.append(getName());
48 | sb.append('\"');
49 | if (value != null) {
50 | sb.append(", value:\"");
51 | sb.append(getValue());
52 | sb.append('\"');
53 | }
54 | sb.append('}');
55 | return sb.toString();
56 | }
57 | }
58 |
59 |
--------------------------------------------------------------------------------
/src/main/java/xyz/gianlu/zeroconf/RecordSRV.java:
--------------------------------------------------------------------------------
1 | package xyz.gianlu.zeroconf;
2 |
3 | import org.jetbrains.annotations.NotNull;
4 |
5 | import java.nio.ByteBuffer;
6 |
7 | public final class RecordSRV extends Record {
8 | private int priority;
9 | private int weight;
10 | private int port;
11 | private String target;
12 |
13 | RecordSRV() {
14 | super(TYPE_SRV);
15 | }
16 |
17 | RecordSRV(String name, String target, int port) {
18 | this();
19 | setName(name);
20 | this.target = target;
21 | this.port = port;
22 | }
23 |
24 | @Override
25 | protected void readData(int len, ByteBuffer in) {
26 | priority = in.getShort() & 0xFFFF;
27 | weight = in.getShort() & 0xFFFF;
28 | port = in.getShort() & 0xFFFF;
29 | target = readName(in);
30 | }
31 |
32 | @Override
33 | protected int writeData(ByteBuffer out, Packet packet) {
34 | if (target != null) {
35 | out.putShort((short) priority);
36 | out.putShort((short) weight);
37 | out.putShort((short) port);
38 | return 6 + writeName(target, out, packet);
39 | } else {
40 | return -1;
41 | }
42 | }
43 |
44 | public int getPriority() {
45 | return priority;
46 | }
47 |
48 | public int getWeight() {
49 | return weight;
50 | }
51 |
52 | public int getPort() {
53 | return port;
54 | }
55 |
56 | public String getTarget() {
57 | return target;
58 | }
59 |
60 | @Override
61 | @NotNull
62 | public String toString() {
63 | StringBuilder sb = new StringBuilder();
64 | sb.append("{type:srv, name:\"");
65 | sb.append(getName());
66 | sb.append('\"');
67 | if (target != null) {
68 | sb.append(", priority:");
69 | sb.append(getPriority());
70 | sb.append(", weight:");
71 | sb.append(getWeight());
72 | sb.append(", port:");
73 | sb.append(getPort());
74 | sb.append(", target:\"");
75 | sb.append(getTarget());
76 | sb.append('\"');
77 | }
78 | sb.append('}');
79 | return sb.toString();
80 | }
81 | }
82 |
83 |
--------------------------------------------------------------------------------
/src/main/java/xyz/gianlu/zeroconf/RecordTXT.java:
--------------------------------------------------------------------------------
1 | package xyz.gianlu.zeroconf;
2 |
3 | import org.jetbrains.annotations.NotNull;
4 |
5 | import java.nio.ByteBuffer;
6 | import java.nio.charset.StandardCharsets;
7 | import java.util.LinkedHashMap;
8 | import java.util.Map;
9 |
10 | public final class RecordTXT extends Record {
11 | private Map values;
12 |
13 | RecordTXT() {
14 | super(TYPE_TXT);
15 | }
16 |
17 | RecordTXT(String name, Map values) {
18 | this();
19 | setName(name);
20 | this.values = values;
21 | }
22 |
23 | RecordTXT(String name, String map) {
24 | this();
25 | setName(name);
26 | values = new LinkedHashMap<>();
27 | String[] q = map.split(", *");
28 | for (String s : q) {
29 | String[] kv = s.split("=");
30 | if (kv.length == 2) values.put(kv[0], kv[1]);
31 | }
32 | }
33 |
34 | @Override
35 | protected void readData(int len, ByteBuffer in) {
36 | int end = in.position() + len;
37 | values = new LinkedHashMap<>();
38 | while (in.position() < end) {
39 | int slen = in.get() & 0xFF;
40 | StringBuilder sb = new StringBuilder(slen);
41 | for (int i = 0; i < slen; i++) sb.append((char) in.get());
42 |
43 | String value = sb.toString();
44 | int ix = value.indexOf("=");
45 | if (ix > 0) values.put(value.substring(0, ix), value.substring(ix + 1));
46 | }
47 | }
48 |
49 | @Override
50 | protected int writeData(ByteBuffer out, Packet packet) {
51 | if (values != null) {
52 | int len = 0;
53 | for (Map.Entry e : values.entrySet()) {
54 | String value = e.getKey() + "=" + e.getValue();
55 | out.put((byte) value.length());
56 | out.put(value.getBytes(StandardCharsets.UTF_8));
57 | len += value.length() + 1;
58 | }
59 | return len;
60 | } else {
61 | return -1;
62 | }
63 | }
64 |
65 | public Map getValues() {
66 | return values;
67 | }
68 |
69 | @Override
70 | @NotNull
71 | public String toString() {
72 | StringBuilder sb = new StringBuilder();
73 | sb.append("{type:text, name:\"");
74 | sb.append(getName());
75 | sb.append("\"");
76 | if (values != null) {
77 | sb.append(", values:");
78 | sb.append(getValues());
79 | }
80 | sb.append('}');
81 | return sb.toString();
82 | }
83 | }
84 |
85 |
--------------------------------------------------------------------------------
/src/main/java/xyz/gianlu/zeroconf/Service.java:
--------------------------------------------------------------------------------
1 | package xyz.gianlu.zeroconf;
2 |
3 | import org.jetbrains.annotations.NotNull;
4 |
5 | import java.net.Inet4Address;
6 | import java.net.Inet6Address;
7 | import java.net.InetAddress;
8 | import java.util.*;
9 |
10 | /**
11 | * Service represents a Service to be announced by the Zeroconf class.
12 | */
13 | @SuppressWarnings({"unused", "WeakerAccess"})
14 | public final class Service {
15 | private final String alias;
16 | private final String service;
17 | private final int port;
18 | private final Map text;
19 | private final List addresses = new ArrayList<>();
20 | private String domain;
21 | private String protocol;
22 | private String host;
23 |
24 | /**
25 | * Create a new {@link Service} to be announced by this object.
26 | *
27 | * A JmDNS `type` field of "_foobar._tcp.local." would be specified here as a `service` param of "foobar".
28 | *
29 | * @param alias the service alias, eg "My Web Server"
30 | * @param service the service type, eg "http"
31 | * @param port the service port
32 | */
33 | public Service(@NotNull String alias, @NotNull String service, int port) {
34 | this.alias = alias;
35 | for (int i = 0; i < alias.length(); i++) {
36 | char c = alias.charAt(i);
37 | if (c < 0x20 || c == 0x7F)
38 | throw new IllegalArgumentException(alias);
39 | }
40 |
41 | this.service = service;
42 | this.port = port;
43 | this.protocol = "tcp";
44 | this.text = new LinkedHashMap<>();
45 | }
46 |
47 | private static void esc(String in, StringBuilder out) {
48 | for (int i = 0; i < in.length(); i++) {
49 | char c = in.charAt(i);
50 | if (c == '.' || c == '\\') out.append('\\');
51 | out.append(c);
52 | }
53 | }
54 |
55 | @Override
56 | public String toString() {
57 | return "Service{" +
58 | "alias='" + alias + '\'' +
59 | ", service='" + service + '\'' +
60 | ", port=" + port +
61 | ", text=" + text +
62 | ", addresses=" + addresses +
63 | ", domain='" + domain + '\'' +
64 | ", protocol='" + protocol + '\'' +
65 | ", host='" + host + '\'' +
66 | '}';
67 | }
68 |
69 | /**
70 | * Set the protocol, which can be one of "tcp" (the default) or "udp"
71 | *
72 | * @param protocol the protocol
73 | * @return this
74 | */
75 | @NotNull
76 | public Service setProtocol(String protocol) {
77 | if ("tcp".equals(protocol) || "udp".equals(protocol)) this.protocol = protocol;
78 | else throw new IllegalArgumentException(protocol);
79 | return this;
80 | }
81 |
82 | /**
83 | * @return the domain
84 | */
85 | public String getDomain() {
86 | return domain;
87 | }
88 |
89 | /**
90 | * Set the domain, which defaults to {@link Zeroconf#getDomain} and must begin with "."
91 | *
92 | * @param domain the domain
93 | * @return this
94 | */
95 | @NotNull
96 | public Service setDomain(String domain) {
97 | if (domain == null || domain.length() < 2 || domain.charAt(0) != '.')
98 | throw new IllegalArgumentException(domain);
99 |
100 | this.domain = domain;
101 | return this;
102 | }
103 |
104 | /**
105 | * @return the host
106 | */
107 | public String getHost() {
108 | return host;
109 | }
110 |
111 | /**
112 | * Set the host which is hosting this service, which defaults to {@link Zeroconf#getLocalHostName}.
113 | * It is possible to announce a service on a non-local host
114 | *
115 | * @param host the host
116 | * @return this
117 | */
118 | @NotNull
119 | public Service setHost(String host) {
120 | this.host = host;
121 | return this;
122 | }
123 |
124 | /**
125 | * Set the Text record to go with this Service, which is of the form "key1=value1, key2=value2"
126 | * Any existing Text records are replaced
127 | *
128 | * @param text the text
129 | * @return this
130 | */
131 | @NotNull
132 | public Service setText(String text) {
133 | this.text.clear();
134 | String[] q = text.split(", *");
135 | for (String s : q) {
136 | String[] r = s.split("=");
137 | if (r.length == 2) this.text.put(r[0], r[1]);
138 | else throw new IllegalArgumentException(text);
139 | }
140 |
141 | return this;
142 | }
143 |
144 | /**
145 | * Set the Text record to go with this Service, which is specified as a Map of keys and values
146 | * Any existing Text records are replaced
147 | *
148 | * @param text the text
149 | * @return this
150 | */
151 | @NotNull
152 | public Service setText(Map text) {
153 | this.text.clear();
154 | this.text.putAll(text);
155 | return this;
156 | }
157 |
158 | /**
159 | * Add a Text record entry to go with this Service to the existing list of Text record entries.
160 | *
161 | * @param key the text key
162 | * @param value the corresponding value.
163 | * @return this
164 | */
165 | @NotNull
166 | public Service putText(String key, String value) {
167 | this.text.put(key, value);
168 | return this;
169 | }
170 |
171 | /**
172 | * Add an InetAddress to the list of addresses for this service. By default they are taken
173 | * from {@link Zeroconf#getLocalAddresses}, as the hostname is taken from {@link Zeroconf#getLocalHostName}.
174 | * If advertising a Service on a non-local host, the addresses must be set manually using this
175 | * method.
176 | *
177 | * @param address the InetAddress this Service resides on
178 | * @return this
179 | */
180 | @NotNull
181 | public Service addAddress(@NotNull InetAddress address) {
182 | addresses.add(address);
183 | return this;
184 | }
185 |
186 | @NotNull
187 | public Service addAddresses(Collection addresses) {
188 | this.addresses.addAll(addresses);
189 | return this;
190 | }
191 |
192 | /**
193 | * @return whether the service has addresses to announce
194 | */
195 | public boolean hasAddresses() {
196 | return !addresses.isEmpty();
197 | }
198 |
199 | /**
200 | * @return the alias
201 | */
202 | @NotNull
203 | public String getAlias() {
204 | return alias;
205 | }
206 |
207 | /**
208 | * Return the instance-name for this service. This is the "fully qualified domain name" of
209 | * the service and looks something like "My Service._http._tcp.local"
210 | *
211 | * @return the instance name
212 | */
213 | @NotNull
214 | public String getInstanceName() {
215 | StringBuilder sb = new StringBuilder();
216 | esc(alias, sb);
217 | sb.append("._");
218 | esc(service, sb);
219 | sb.append("._");
220 | sb.append(protocol);
221 | sb.append(domain);
222 | return sb.toString();
223 | }
224 |
225 | /**
226 | * Return the service-name for this service. This is the "domain name" of
227 | * the service and looks something like "._http._tcp.local" - i.e. the InstanceName
228 | * without the alias. Note the rather ambiguous term "service name" comes from the spec.
229 | *
230 | * @return the service name
231 | */
232 | @NotNull
233 | public String getServiceName() {
234 | StringBuilder sb = new StringBuilder();
235 | sb.append('_');
236 | esc(service, sb);
237 | sb.append("._");
238 | sb.append(protocol);
239 | sb.append(domain);
240 | return sb.toString();
241 | }
242 |
243 | @NotNull
244 | Packet getPacket() {
245 | Packet packet = new Packet();
246 | packet.setAuthoritative(true);
247 |
248 | String fqdn = getInstanceName();
249 | String ptrname = getServiceName();
250 |
251 | packet.addAnswer(new RecordPTR(ptrname, fqdn).setTTL(28800));
252 | packet.addAnswer(new RecordSRV(fqdn, host, port).setTTL(120));
253 | if (!text.isEmpty()) packet.addAnswer(new RecordTXT(fqdn, text).setTTL(120));
254 |
255 | for (InetAddress address : addresses) {
256 | if (address instanceof Inet4Address)
257 | packet.addAnswer(new RecordA(host, (Inet4Address) address));
258 | else if (address instanceof Inet6Address)
259 | packet.addAnswer(new RecordAAAA(host, (Inet6Address) address));
260 | }
261 |
262 | return packet;
263 | }
264 | }
265 |
--------------------------------------------------------------------------------
/src/main/java/xyz/gianlu/zeroconf/Zeroconf.java:
--------------------------------------------------------------------------------
1 | package xyz.gianlu.zeroconf;
2 |
3 | import org.jetbrains.annotations.NotNull;
4 | import org.jetbrains.annotations.Nullable;
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 |
8 | import java.io.Closeable;
9 | import java.io.IOException;
10 | import java.math.BigInteger;
11 | import java.net.*;
12 | import java.nio.ByteBuffer;
13 | import java.nio.ByteOrder;
14 | import java.nio.channels.DatagramChannel;
15 | import java.nio.channels.SelectionKey;
16 | import java.nio.channels.Selector;
17 | import java.util.*;
18 | import java.util.concurrent.CopyOnWriteArrayList;
19 | import java.util.concurrent.ThreadLocalRandom;
20 | import java.util.concurrent.TimeUnit;
21 | import java.util.concurrent.atomic.AtomicBoolean;
22 |
23 | /**
24 | *
25 | * This is the root class for the Service Discovery object.
26 | *
27 | * This class does not have any fancy hooks to clean up. The {@link #close} method should be called when the
28 | * class is to be discarded, but failing to do so won't break anything. Announced services will expire in
29 | * their own time, which is typically two minutes - although during this time, conforming implementations
30 | * should refuse to republish any duplicate services.
31 | *
32 | */
33 | @SuppressWarnings({"unused", "WeakerAccess"})
34 | public final class Zeroconf implements Closeable {
35 | private static final String DISCOVERY = "_services._dns-sd._udp.local";
36 | private static final InetSocketAddress BROADCAST4;
37 | private static final InetSocketAddress BROADCAST6;
38 | private static final Logger LOGGER = LoggerFactory.getLogger(Zeroconf.class);
39 |
40 | static {
41 | try {
42 | BROADCAST4 = new InetSocketAddress(InetAddress.getByName("224.0.0.251"), 5353);
43 | BROADCAST6 = new InetSocketAddress(InetAddress.getByName("FF02::FB"), 5353);
44 | } catch (IOException ex) {
45 | throw new IllegalStateException(ex);
46 | }
47 | }
48 |
49 | private final ListenerThread thread;
50 | private final List registry;
51 | private final Collection services;
52 | private final CopyOnWriteArrayList discoverers;
53 | private final CopyOnWriteArrayList receiveListeners;
54 | private final CopyOnWriteArrayList sendListeners;
55 | private boolean useIpv4 = true;
56 | private boolean useIpv6 = true;
57 | private String hostname;
58 | private String domain;
59 |
60 | /**
61 | * Create a new Zeroconf object
62 | */
63 | public Zeroconf() {
64 | setDomain(".local");
65 |
66 | try {
67 | setLocalHostName(getOrCreateLocalHostName());
68 | } catch (IOException ignored) {
69 | }
70 |
71 | receiveListeners = new CopyOnWriteArrayList<>();
72 | sendListeners = new CopyOnWriteArrayList<>();
73 | discoverers = new CopyOnWriteArrayList<>();
74 | thread = new ListenerThread();
75 | registry = new ArrayList<>();
76 | services = new HashSet<>();
77 | }
78 |
79 | @NotNull
80 | public static String getOrCreateLocalHostName() throws UnknownHostException {
81 | String host = InetAddress.getLocalHost().getHostName();
82 | if (Objects.equals(host, "localhost")) {
83 | host = Base64.getEncoder().encodeToString(BigInteger.valueOf(ThreadLocalRandom.current().nextLong()).toByteArray()) + ".local";
84 | LOGGER.warn("Hostname cannot be `localhost`, temporary hostname is {}.", host);
85 | return host;
86 | }
87 |
88 | return host;
89 | }
90 |
91 | @NotNull
92 | public Zeroconf setUseIpv4(boolean ipv4) {
93 | this.useIpv4 = ipv4;
94 | return this;
95 | }
96 |
97 | @NotNull
98 | public Zeroconf setUseIpv6(boolean ipv6) {
99 | this.useIpv6 = ipv6;
100 | return this;
101 | }
102 |
103 | /**
104 | * Close down this Zeroconf object and cancel any services it has advertised.
105 | */
106 | @Override
107 | public void close() {
108 | for (Service service : new ArrayList<>(services))
109 | unannounce(service);
110 |
111 | services.clear();
112 |
113 | for (DiscoveredServices discoverer : new ArrayList<>(discoverers))
114 | discoverer.stop();
115 |
116 | discoverers.clear();
117 |
118 | try {
119 | thread.close();
120 | } catch (InterruptedException ignored) {
121 | }
122 | }
123 |
124 | /**
125 | * Add a {@link PacketListener} to the list of listeners notified when a Service Discovery
126 | * Packet is received
127 | *
128 | * @param listener the listener
129 | * @return this Zeroconf
130 | */
131 | @NotNull
132 | public Zeroconf addReceiveListener(@NotNull PacketListener listener) {
133 | receiveListeners.addIfAbsent(listener);
134 | return this;
135 | }
136 |
137 | /**
138 | * Remove a previously added {@link PacketListener} from the list of listeners notified when
139 | * a Service Discovery Packet is received
140 | *
141 | * @param listener the listener
142 | * @return this Zeroconf
143 | */
144 | @NotNull
145 | public Zeroconf removeReceiveListener(@NotNull PacketListener listener) {
146 | receiveListeners.remove(listener);
147 | return this;
148 | }
149 |
150 | /**
151 | * Add a {@link PacketListener} to the list of listeners notified when a Service
152 | * Discovery Packet is sent
153 | *
154 | * @param listener the listener
155 | * @return this Zeroconf
156 | */
157 | @NotNull
158 | public Zeroconf addSendListener(@NotNull PacketListener listener) {
159 | sendListeners.addIfAbsent(listener);
160 | return this;
161 | }
162 |
163 | /**
164 | * Remove a previously added {@link PacketListener} from the list of listeners notified
165 | * when a Service Discovery Packet is sent
166 | *
167 | * @param listener the listener
168 | * @return this Zeroconf
169 | */
170 | @NotNull
171 | public Zeroconf removeSendListener(@NotNull PacketListener listener) {
172 | sendListeners.remove(listener);
173 | return this;
174 | }
175 |
176 | /**
177 | *
178 | * Add a {@link NetworkInterface} to the list of interfaces that send and received Service
179 | * Discovery Packets. The interface should be up, should
180 | * {@link NetworkInterface#supportsMulticast} support Multicast and not be a
181 | * {@link NetworkInterface#isLoopback Loopback interface}. However, adding a
182 | * NetworkInterface that does not match this requirement will not throw an Exception - it
183 | * will just be ignored, as will any attempt to add a NetworkInterface that has already
184 | * been added.
185 | *
186 | * All the interface's IP addresses will be added to the list of
187 | * {@link #getLocalAddresses local addresses}.
188 | * If the interface's addresses change, or the interface is otherwise modified in a
189 | * significant way, then it should be removed and re-added to this object. This is
190 | * not done automatically.
191 | *
192 | *
193 | * @param nic a NetworkInterface
194 | * @return this
195 | * @throws IOException if something goes wrong in an I/O way
196 | */
197 | @NotNull
198 | public Zeroconf addNetworkInterface(@NotNull NetworkInterface nic) throws IOException {
199 | thread.addNetworkInterface(nic);
200 | return this;
201 | }
202 |
203 | @NotNull
204 | public Zeroconf addNetworkInterfaces(@NotNull Collection nics) throws IOException {
205 | for (NetworkInterface nic : nics) thread.addNetworkInterface(nic);
206 | return this;
207 | }
208 |
209 | /**
210 | * Remove a {@link #addNetworkInterface previously added} NetworkInterface from this
211 | * object's list. The addresses that were part of the interface at the time it was added
212 | * will be removed from the list of {@link #getLocalAddresses local addresses}.
213 | *
214 | * @param nic a NetworkInterface
215 | * @return this
216 | * @throws IOException if something goes wrong in an I/O way
217 | */
218 | @NotNull
219 | public Zeroconf removeNetworkInterface(@NotNull NetworkInterface nic) throws IOException {
220 | thread.removeNetworkInterface(nic);
221 | return this;
222 | }
223 |
224 | /**
225 | * A convenience method to add all local NetworkInterfaces - it simply runs
226 | *
227 | * for (Enumeration<NetworkInterface> e = NetworkInterface.getNetworkInterfaces();e.hasMoreElements();) {
228 | * addNetworkInterface(e.nextElement());
229 | * }
230 | *
231 | *
232 | * @return this
233 | * @throws IOException if something goes wrong in an I/O way
234 | */
235 | @NotNull
236 | public Zeroconf addAllNetworkInterfaces() throws IOException {
237 | for (Enumeration e = NetworkInterface.getNetworkInterfaces(); e.hasMoreElements(); )
238 | addNetworkInterface(e.nextElement());
239 |
240 | return this;
241 | }
242 |
243 | /**
244 | * Get the Service Discovery Domain, which is set by {@link #setDomain}. It defaults to ".local",
245 | * but can be set by {@link #setDomain}
246 | *
247 | * @return the domain
248 | */
249 | public String getDomain() {
250 | return domain;
251 | }
252 |
253 | /**
254 | * Set the Service Discovery Domain
255 | *
256 | * @param domain the domain
257 | * @return this
258 | */
259 | @NotNull
260 | public Zeroconf setDomain(@NotNull String domain) {
261 | this.domain = domain;
262 | return this;
263 | }
264 |
265 | /**
266 | * Get the local hostname, which defaults to InetAddress.getLocalHost().getHostName()
.
267 | *
268 | * @return the local host name
269 | */
270 | public String getLocalHostName() {
271 | if (hostname == null) throw new IllegalStateException("Hostname cannot be determined");
272 | return hostname;
273 | }
274 |
275 | /**
276 | * Set the local hostname, as returned by {@link #getOrCreateLocalHostName}
277 | *
278 | * @param name the hostname, which should be undotted
279 | * @return this
280 | */
281 | @NotNull
282 | public Zeroconf setLocalHostName(@NotNull String name) {
283 | this.hostname = name;
284 | return this;
285 | }
286 |
287 | /**
288 | * Return a list of InetAddresses which the Zeroconf object considers to be "local". These
289 | * are the all the addresses of all the {@link NetworkInterface} objects added to this
290 | * object. The returned list is a copy, it can be modified and will not be updated
291 | * by this object.
292 | *
293 | * @return a List of local {@link InetAddress} objects
294 | */
295 | public List getLocalAddresses() {
296 | return thread.getLocalAddresses();
297 | }
298 |
299 | /**
300 | * Send a packet
301 | */
302 | public void send(@NotNull Packet packet) {
303 | thread.push(packet);
304 | }
305 |
306 | /**
307 | * Return the registry of records. This is the list of DNS records that we will
308 | * automatically match any queries against. The returned list is live.
309 | */
310 | public List getRegistry() {
311 | return registry;
312 | }
313 |
314 | /**
315 | * Return the list of all Services that have been {@link Zeroconf#announce} announced
316 | * by this object. The returned Collection is read-only and live, so will be updated
317 | * by this object.
318 | *
319 | * @return the Collection of announced Services
320 | */
321 | public Collection getAnnouncedServices() {
322 | return Collections.unmodifiableCollection(services);
323 | }
324 |
325 | /**
326 | * Given a query packet, trawl through our registry and try to find any records that
327 | * match the queries. If there are any, send our own response packet.
328 | *
329 | * This is largely derived from other implementations, but broadly the logic here is
330 | * that questions are matched against records based on the "name" and "type" fields,
331 | * where {@link #DISCOVERY} and {@link Record#TYPE_ANY} are wildcards for those
332 | * fields. Currently we match against all packet types - should these be just "PTR"
333 | * records?
334 | *
335 | * Once we have this list of matched records, we search this list for any PTR records
336 | * and add any matching SRV or TXT records (RFC 6763 12.1). After that, we scan our
337 | * updated list and add any A or AAAA records that match any SRV records (12.2).
338 | *
339 | * At the end of all this, if we have at least one record, send it as a response
340 | */
341 | private void handlePacket(@NotNull Packet packet) {
342 | Packet response = null;
343 | Set targets = null;
344 | for (Record question : packet.getQuestions()) {
345 | for (Record record : getRegistry()) {
346 | if ((question.getName().equals(DISCOVERY) || question.getName().equals(record.getName())) && (question.getType() == record.getType() || question.getType() == Record.TYPE_ANY && record.getType() != Record.TYPE_NSEC)) {
347 | if (response == null) {
348 | response = new Packet(packet.getID());
349 | response.setAuthoritative(true);
350 | }
351 |
352 | response.addAnswer(record);
353 | if (record instanceof RecordSRV) {
354 | if (targets == null) targets = new HashSet<>();
355 | targets.add(((RecordSRV) record).getTarget());
356 | }
357 | }
358 | }
359 |
360 | if (response != null && question.getType() != Record.TYPE_ANY) {
361 | // When including a DNS-SD Service Instance Enumeration or Selective
362 | // Instance Enumeration (subtype) PTR record in a response packet, the
363 | // server/responder SHOULD include the following additional records:
364 | // o The SRV record(s) named in the PTR rdata.
365 | // o The TXT record(s) named in the PTR rdata.
366 | // o All address records (type "A" and "AAAA") named in the SRV rdata.
367 | for (Record answer : response.getAnswers()) {
368 | if (answer.getType() != Record.TYPE_PTR)
369 | continue;
370 |
371 | for (Record record : getRegistry()) {
372 | if (record.getName().equals(((RecordPTR) answer).getValue())
373 | && (record.getType() == Record.TYPE_SRV || record.getType() == Record.TYPE_TXT)) {
374 | response.addAdditional(record);
375 | if (record instanceof RecordSRV) {
376 | if (targets == null) targets = new HashSet<>();
377 | targets.add(((RecordSRV) record).getTarget());
378 | }
379 | }
380 | }
381 | }
382 | }
383 | }
384 |
385 | if (response != null) {
386 | // When including an SRV record in a response packet, the
387 | // server/responder SHOULD include the following additional records:
388 | // o All address records (type "A" and "AAAA") named in the SRV rdata.
389 | if (targets != null) {
390 | for (String target : targets) {
391 | for (Record record : getRegistry()) {
392 | if (record.getName().equals(target) && (record.getType() == Record.TYPE_A || record.getType() == Record.TYPE_AAAA)) {
393 | response.addAdditional(record);
394 | }
395 | }
396 | }
397 | }
398 |
399 | send(response);
400 | }
401 | }
402 |
403 | /**
404 | * Create a background thread that continuously searches for the given service.
405 | *
406 | * @param service the service name, eg "_http"
407 | * @param protocol the protocol, eg "_tcp"
408 | * @param domain the domain, eg ".local"
409 | * @return a list of discovered services
410 | */
411 | @NotNull
412 | public DiscoveredServices discover(@NotNull String service, @NotNull String protocol, @NotNull String domain) {
413 | DiscoveredServices discoverer = new DiscoveredServices("_" + service + "._" + protocol + domain);
414 | new Thread(discoverer, "zeroconf-discover-" + service + "-" + protocol + "-" + domain).start();
415 | discoverers.add(discoverer);
416 | return discoverer;
417 | }
418 |
419 | /**
420 | * Probe for a ZeroConf service with the specified name and return true if a matching
421 | * service is found.
422 | *
423 | * The approach is borrowed from https://www.npmjs.com/package/bonjour - we send three
424 | * broadcasts trying to match the service name, 250ms apart. If we receive no response,
425 | * assume there is no service that matches
426 | *
427 | * Note the approach here is the only example of where we send a query packet. It could
428 | * be used as the basis for us acting as a service discovery client
429 | *
430 | * @param fqdn the fully qualified service name, eg "My Web Service._http._tcp.local".
431 | */
432 | private boolean probe(final String fqdn) {
433 | Packet probe = new Packet();
434 | probe.setResponse(false);
435 | probe.addQuestion(new RecordANY(fqdn));
436 | AtomicBoolean match = new AtomicBoolean(false);
437 | PacketListener probeListener = packet -> {
438 | if (packet.isResponse()) {
439 | for (Record r : packet.getAnswers()) {
440 | if (r.getName().equalsIgnoreCase(fqdn)) {
441 | synchronized (match) {
442 | match.set(true);
443 | match.notifyAll();
444 | }
445 | }
446 | }
447 |
448 | for (Record r : packet.getAdditionals()) {
449 | if (r.getName().equalsIgnoreCase(fqdn)) {
450 | synchronized (match) {
451 | match.set(true);
452 | match.notifyAll();
453 | }
454 | }
455 | }
456 | }
457 | };
458 |
459 | addReceiveListener(probeListener);
460 | for (int i = 0; i < 3 && !match.get(); i++) {
461 | send(probe);
462 | synchronized (match) {
463 | try {
464 | match.wait(250);
465 | } catch (InterruptedException ex) {
466 | // ignore
467 | }
468 | }
469 | }
470 |
471 | removeReceiveListener(probeListener);
472 | return match.get();
473 | }
474 |
475 | /**
476 | * Announce the service - probe to see if it already exists and fail if it does, otherwise
477 | * announce it
478 | */
479 | public void announce(@NotNull Service service) {
480 | if (service.getDomain() == null) service.setDomain(getDomain());
481 | if (service.getHost() == null) service.setHost(getLocalHostName());
482 | if (!service.hasAddresses()) service.addAddresses(getLocalAddresses());
483 |
484 | Packet packet = service.getPacket();
485 | if (probe(service.getInstanceName()))
486 | throw new IllegalArgumentException("Service " + service.getInstanceName() + " already on network");
487 |
488 | getRegistry().addAll(packet.getAnswers());
489 | services.add(service);
490 |
491 | for (int i = 0; i < 3; i++) {
492 | send(packet);
493 |
494 | try {
495 | Thread.sleep(225);
496 | } catch (InterruptedException ignored) {
497 | }
498 | }
499 |
500 | LOGGER.info("Announced {}.", service);
501 | }
502 |
503 | /**
504 | * Unannounce the service. Do this by re-announcing all our records but with a TTL of 0 to
505 | * ensure they expire. Then remove from the registry.
506 | */
507 | public void unannounce(@NotNull Service service) {
508 | Packet packet = service.getPacket();
509 | getRegistry().removeAll(packet.getAnswers());
510 | for (Record r : packet.getAnswers()) {
511 | getRegistry().remove(r);
512 | r.setTTL(0);
513 | }
514 |
515 | services.remove(service);
516 |
517 | for (int i = 0; i < 3; i++) {
518 | send(packet);
519 |
520 | try {
521 | Thread.sleep(125);
522 | } catch (InterruptedException ignored) {
523 | }
524 | }
525 |
526 | LOGGER.info("Unannounced {}.", service);
527 | }
528 |
529 | public class DiscoveredServices implements Runnable {
530 | private final String serviceName;
531 | private final PacketListener listener;
532 | private final Set services = Collections.synchronizedSet(new HashSet<>());
533 | private volatile boolean shouldStop = false;
534 | private int nextInterval = 1000;
535 |
536 | DiscoveredServices(@NotNull String serviceName) {
537 | this.serviceName = serviceName;
538 |
539 | addReceiveListener(listener = packet -> {
540 | if (!packet.isResponse())
541 | return;
542 |
543 | for (Record r : packet.getAnswers()) {
544 | if (r instanceof RecordSRV) addService((RecordSRV) r);
545 | else addRelated(r);
546 | }
547 |
548 | for (Record r : packet.getAdditionals()) {
549 | if (r instanceof RecordSRV) addService((RecordSRV) r);
550 | else addRelated(r);
551 | }
552 | });
553 | }
554 |
555 | private void addRelated(@NotNull Record record) {
556 | if (!record.getName().endsWith(serviceName))
557 | return;
558 |
559 | synchronized (services) {
560 | services.stream().filter(s -> s.serviceName.equals(record.getName())).forEach(s -> s.addRelatedRecord(record));
561 | }
562 | }
563 |
564 | private void addService(@NotNull RecordSRV record) {
565 | if (!record.getName().endsWith(serviceName))
566 | return;
567 |
568 | services.removeIf(s -> s.isExpired() || s.serviceName.equals(record.getName()));
569 | services.add(new DiscoveredService(record));
570 | }
571 |
572 | public void stop() {
573 | shouldStop = true;
574 | }
575 |
576 | @NotNull
577 | public List getServices() {
578 | return new ArrayList<>(services);
579 | }
580 |
581 | @Nullable
582 | public DiscoveredService getService(@NotNull String name) {
583 | synchronized (services) {
584 | for (DiscoveredService service : services)
585 | if (service.name.equals(name))
586 | return service;
587 |
588 | return null;
589 | }
590 | }
591 |
592 | @Override
593 | public void run() {
594 | while (!shouldStop) {
595 | Packet probe = new Packet();
596 | probe.setResponse(false);
597 | probe.addQuestion(new RecordPTR(serviceName));
598 | send(probe);
599 |
600 | try {
601 | //noinspection BusyWait
602 | Thread.sleep(nextInterval);
603 | nextInterval *= 2;
604 | if (nextInterval >= TimeUnit.MINUTES.toMillis(60))
605 | nextInterval = (int) (TimeUnit.MINUTES.toMillis(60) + 20 + Math.random() * 100);
606 | } catch (InterruptedException ex) {
607 | break;
608 | }
609 | }
610 |
611 | removeReceiveListener(listener);
612 | }
613 | }
614 |
615 | /**
616 | * The thread that listens to one or more Multicast DatagramChannels using a Selector,
617 | * waiting for incoming packets. This wait can be also interrupted and a packet sent.
618 | */
619 | private class ListenerThread extends Thread {
620 | private final Deque sendq;
621 | private final Map channels;
622 | private final Map> localAddresses;
623 | private final ABLock selectorLock = new ABLock();
624 | private volatile boolean cancelled;
625 | private Selector selector;
626 |
627 | ListenerThread() {
628 | super("zeroconf-io-thread");
629 |
630 | setDaemon(false);
631 | sendq = new ArrayDeque<>();
632 | channels = new HashMap<>();
633 | localAddresses = new HashMap<>();
634 | }
635 |
636 | private synchronized Selector getSelector() throws IOException {
637 | if (selector == null)
638 | selector = Selector.open();
639 | return selector;
640 | }
641 |
642 | /**
643 | * Stop the thread and rejoin
644 | */
645 | void close() throws InterruptedException {
646 | this.cancelled = true;
647 | if (selector != null) {
648 | selector.wakeup();
649 | if (isAlive()) join();
650 | }
651 | }
652 |
653 | /**
654 | * Add a packet to the send queue
655 | */
656 | void push(Packet packet) {
657 | sendq.addLast(packet);
658 | if (selector != null) {
659 | // Only send if we have a Nic
660 | selector.wakeup();
661 | }
662 | }
663 |
664 | /**
665 | * Pop a packet from the send queue or return null if none available
666 | */
667 | private Packet pop() {
668 | return sendq.pollFirst();
669 | }
670 |
671 | /**
672 | * Add a NetworkInterface. Try to identify whether it's IPV4 or IPV6, or both. IPV4 tested,
673 | * IPV6 is not but at least it doesn't crash.
674 | */
675 | public void addNetworkInterface(@NotNull NetworkInterface nic) throws IOException {
676 | if (!channels.containsKey(nic) && nic.supportsMulticast() && nic.isUp() && !nic.isLoopback()) {
677 | boolean ipv4 = false;
678 | boolean ipv6 = false;
679 | List locallist = new ArrayList<>();
680 | for (Enumeration e = nic.getInetAddresses(); e.hasMoreElements(); ) {
681 | InetAddress a = e.nextElement();
682 | if ((a instanceof Inet4Address && !useIpv4) || (a instanceof Inet6Address && !useIpv6))
683 | continue;
684 |
685 | ipv4 |= a instanceof Inet4Address;
686 | ipv6 |= a instanceof Inet6Address;
687 | if (!a.isLoopbackAddress() && !a.isMulticastAddress())
688 | locallist.add(a);
689 | }
690 |
691 | DatagramChannel channel = DatagramChannel.open(StandardProtocolFamily.INET);
692 | channel.configureBlocking(false);
693 | channel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
694 | channel.setOption(StandardSocketOptions.IP_MULTICAST_TTL, 255);
695 | if (ipv4) {
696 | channel.bind(new InetSocketAddress("0.0.0.0", BROADCAST4.getPort()));
697 | channel.setOption(StandardSocketOptions.IP_MULTICAST_IF, nic);
698 | channel.join(BROADCAST4.getAddress(), nic);
699 | } else if (ipv6) {
700 | channel.bind(new InetSocketAddress("::", BROADCAST6.getPort()));
701 | channel.join(BROADCAST6.getAddress(), nic);
702 | }
703 |
704 | selectorLock.lockA1();
705 | try {
706 | getSelector().wakeup();
707 |
708 | selectorLock.lockA2();
709 | try {
710 | channels.put(nic, channel.register(getSelector(), SelectionKey.OP_READ));
711 | } finally {
712 | selectorLock.unlockA2();
713 | }
714 | } catch (InterruptedException ignored) {
715 | } finally {
716 | selectorLock.unlockA1();
717 | }
718 |
719 | localAddresses.put(nic, locallist);
720 | if (!isAlive()) start();
721 | }
722 | }
723 |
724 | void removeNetworkInterface(@NotNull NetworkInterface nic) throws IOException {
725 | SelectionKey key = channels.remove(nic);
726 | if (key != null) {
727 | localAddresses.remove(nic);
728 | key.channel().close();
729 | getSelector().wakeup();
730 | }
731 | }
732 |
733 | List getLocalAddresses() {
734 | List list = new ArrayList<>();
735 | for (List pernic : localAddresses.values()) {
736 | for (InetAddress address : pernic) {
737 | if (!list.contains(address))
738 | list.add(address);
739 | }
740 | }
741 |
742 | return list;
743 | }
744 |
745 | @Override
746 | public void run() {
747 | ByteBuffer buf = ByteBuffer.allocate(65536);
748 | buf.order(ByteOrder.BIG_ENDIAN);
749 | while (!cancelled) {
750 | buf.clear();
751 | try {
752 | Packet packet = pop();
753 | if (packet != null) {
754 | // Packet to Send
755 | buf.clear();
756 | packet.write(buf);
757 | buf.flip();
758 | for (PacketListener listener : sendListeners)
759 | listener.packetEvent(packet);
760 |
761 | for (SelectionKey key : channels.values()) {
762 | DatagramChannel channel = (DatagramChannel) key.channel();
763 | InetSocketAddress address = packet.getAddress();
764 | if (address != null) {
765 | buf.position(0);
766 | channel.send(buf, address);
767 | } else {
768 | if (useIpv4) {
769 | buf.position(0);
770 | channel.send(buf, BROADCAST4);
771 | }
772 |
773 | if (useIpv6) {
774 | buf.position(0);
775 | channel.send(buf, BROADCAST6);
776 | }
777 | }
778 | }
779 | }
780 |
781 | // We know selector exists
782 | Selector selector = getSelector();
783 | selectorLock.lockB();
784 | try {
785 | selector.select();
786 | } finally {
787 | selectorLock.unlockB();
788 | }
789 |
790 | Set selected = selector.selectedKeys();
791 | for (SelectionKey key : selected) {
792 | // We know selected keys are readable
793 | DatagramChannel channel = (DatagramChannel) key.channel();
794 | InetSocketAddress address = (InetSocketAddress) channel.receive(buf);
795 | if (address != null && buf.position() != 0) {
796 | buf.flip();
797 | packet = new Packet();
798 | packet.read(buf, address);
799 | for (PacketListener listener : receiveListeners)
800 | listener.packetEvent(packet);
801 | handlePacket(packet);
802 | }
803 | }
804 |
805 | selected.clear();
806 | } catch (Exception ex) {
807 | LOGGER.warn("Failed receiving/sending packet!", ex);
808 | }
809 | }
810 | }
811 | }
812 | }
813 |
--------------------------------------------------------------------------------