Commit | Line | Data |
---|---|---|
a046fa49 NR |
1 | package be.nikiroo.jvcard.remote; |
2 | ||
3 | import java.io.File; | |
4 | import java.io.IOException; | |
5 | import java.net.ServerSocket; | |
6 | import java.net.Socket; | |
7 | import java.net.UnknownHostException; | |
8 | import java.util.Date; | |
9 | import java.util.LinkedList; | |
10 | import java.util.List; | |
11 | import java.util.ResourceBundle; | |
12 | ||
13 | import be.nikiroo.jvcard.Card; | |
14 | import be.nikiroo.jvcard.parsers.Format; | |
15 | import be.nikiroo.jvcard.parsers.Parser; | |
16 | import be.nikiroo.jvcard.remote.Command.Verb; | |
17 | import be.nikiroo.jvcard.resources.Bundles; | |
18 | import be.nikiroo.jvcard.tui.StringUtils; | |
19 | ||
20 | /** | |
21 | * This class implements a small server that can listen for requests to | |
22 | * synchronise, get and put {@link Card}s. | |
23 | * | |
24 | * <p> | |
25 | * It is <b>NOT</b> secured in any way (it even is nice enough to give you a | |
26 | * help message when you connect in raw mode via nc on how to use it), so do | |
27 | * <b>NOT</b> enable such a server to be accessible from internet. This is not | |
28 | * safe. Use a ssh/openssl tunnel or similar. | |
29 | * </p> | |
30 | * | |
31 | * @author niki | |
32 | * | |
33 | */ | |
34 | public class Server implements Runnable { | |
35 | private ServerSocket ss; | |
36 | private int port; | |
37 | private boolean stop; | |
38 | private File dataDir; | |
39 | ||
40 | private Object clientsLock = new Object(); | |
41 | private List<SimpleSocket> clients = new LinkedList<SimpleSocket>(); | |
42 | ||
43 | private Object cardsLock = new Object(); | |
44 | ||
45 | public static void main(String[] args) throws IOException { | |
46 | Server server = new Server(4444); | |
47 | server.run(); | |
48 | } | |
49 | ||
50 | /** | |
51 | * Create a new jVCard sercer on the given port. | |
52 | * | |
53 | * @param port | |
54 | * the port to run on | |
55 | * | |
56 | * @throws IOException | |
57 | * in case of IO error | |
58 | */ | |
59 | public Server(int port) throws IOException { | |
60 | this.port = port; | |
61 | ResourceBundle bundle = Bundles.getBundle("remote"); | |
62 | try { | |
63 | String dir = bundle.getString("SERVER_DATA_PATH"); | |
64 | dataDir = new File(dir); | |
65 | dataDir.mkdir(); | |
66 | ||
67 | if (!dataDir.exists()) { | |
68 | throw new IOException("Cannot open or create data store at: " | |
69 | + dataDir); | |
70 | } | |
71 | } catch (Exception e) { | |
72 | e.printStackTrace(); | |
73 | throw new IOException("Cannot open or create data store at: " | |
74 | + dataDir, e); | |
75 | } | |
76 | ||
77 | ss = new ServerSocket(port); | |
78 | } | |
79 | ||
80 | /** | |
81 | * Stop the server. It may take some time before returning, but will only | |
82 | * return when the server is actually stopped. | |
83 | */ | |
84 | public void stop() { | |
85 | stop = true; | |
86 | try { | |
87 | SimpleSocket c = new SimpleSocket(new Socket((String) null, port), | |
88 | "special STOP client"); | |
89 | c.open(true); | |
90 | c.sendCommand(Verb.STOP); | |
91 | c.close(); | |
92 | } catch (UnknownHostException e) { | |
93 | e.printStackTrace(); | |
94 | } catch (IOException e) { | |
95 | e.printStackTrace(); | |
96 | } | |
97 | ||
98 | if (clients.size() > 0) { | |
99 | try { | |
100 | Thread.sleep(100); | |
101 | } catch (InterruptedException e) { | |
102 | } | |
103 | ||
104 | if (clients.size() > 0) { | |
105 | synchronized (clientsLock) { | |
106 | for (SimpleSocket s : clients) { | |
107 | System.err | |
108 | .println("Forcefully closing client connection"); | |
109 | s.close(); | |
110 | } | |
111 | ||
112 | clients.clear(); | |
113 | } | |
114 | } | |
115 | } | |
116 | } | |
117 | ||
118 | @Override | |
119 | public void run() { | |
120 | while (!stop) { | |
121 | try { | |
122 | final Socket s = ss.accept(); | |
123 | // TODO: thread pool? | |
124 | new Thread(new Runnable() { | |
125 | @Override | |
126 | public void run() { | |
127 | accept(new SimpleSocket(s, "[request]")); | |
128 | } | |
129 | }).start(); | |
130 | } catch (IOException ioe) { | |
131 | ioe.printStackTrace(); | |
132 | } | |
133 | } | |
134 | } | |
135 | ||
136 | /** | |
137 | * Add a client to the current count. | |
138 | * | |
139 | * @return the client index number | |
140 | */ | |
141 | private void addClient(SimpleSocket s) { | |
142 | synchronized (clientsLock) { | |
143 | clients.add(s); | |
144 | } | |
145 | } | |
146 | ||
147 | /** | |
148 | * Remove a client from the current count. | |
149 | * | |
150 | * @param index | |
151 | * the client index number | |
152 | */ | |
153 | private void removeClient(SimpleSocket s) { | |
154 | synchronized (clientsLock) { | |
155 | clients.remove(s); | |
156 | } | |
157 | } | |
158 | ||
159 | /** | |
160 | * Accept a client and process it. | |
161 | * | |
162 | * @param s | |
163 | * the client to process | |
164 | */ | |
165 | private void accept(SimpleSocket s) { | |
166 | addClient(s); | |
167 | ||
168 | try { | |
169 | s.open(false); | |
170 | ||
171 | boolean clientStop = false; | |
172 | while (!clientStop) { | |
173 | Command cmd = s.receiveCommand(); | |
174 | Command.Verb verb = cmd.getVerb(); | |
175 | ||
176 | if (verb == null) | |
177 | break; | |
178 | ||
179 | System.out.println(s + " -> " + verb); | |
180 | ||
181 | switch (verb) { | |
182 | case STOP: | |
183 | clientStop = true; | |
184 | break; | |
185 | case VERSION: | |
186 | s.sendCommand(Verb.VERSION); | |
187 | break; | |
188 | case TIME: | |
189 | s.sendLine(StringUtils.fromTime(new Date().getTime())); | |
190 | break; | |
191 | case GET: | |
192 | synchronized (cardsLock) { | |
193 | s.sendBlock(doGetCard(cmd.getParam())); | |
194 | } | |
195 | break; | |
196 | case POST: | |
197 | synchronized (cardsLock) { | |
198 | doPostCard(cmd.getParam(), s.receiveBlock()); | |
199 | s.sendBlock(); | |
200 | break; | |
201 | } | |
202 | case LIST: | |
203 | for (File file : dataDir.listFiles()) { | |
204 | if (cmd.getParam() == null | |
205 | || cmd.getParam().length() == 0 | |
206 | || file.getName().contains(cmd.getParam())) { | |
207 | s.send(StringUtils.fromTime(file.lastModified()) | |
208 | + " " + file.getName()); | |
209 | } | |
210 | } | |
211 | s.sendBlock(); | |
212 | break; | |
213 | case HELP: | |
214 | // TODO: i18n | |
215 | s.send("The following commands are available:"); | |
216 | s.send("- TIME: get the server time"); | |
217 | s.send("- HELP: this help screen"); | |
218 | s.send("- LIST: list the available cards on this server"); | |
219 | s.send("- VERSION/GET/PUT/POST/DELETE/STOP: TODO"); | |
220 | s.sendBlock(); | |
221 | break; | |
222 | default: | |
223 | System.err | |
224 | .println("Unsupported command received from a client connection, closing it: " | |
225 | + verb); | |
226 | clientStop = true; | |
227 | break; | |
228 | } | |
229 | } | |
230 | } catch (IOException e) { | |
231 | e.printStackTrace(); | |
232 | } finally { | |
233 | s.close(); | |
234 | } | |
235 | ||
236 | removeClient(s); | |
237 | } | |
238 | ||
239 | /** | |
240 | * Return the serialised {@link Card} (with timestamp). | |
241 | * | |
242 | * @param name | |
243 | * the resource name to load | |
244 | * | |
245 | * @return the serialised data | |
246 | * | |
247 | * @throws IOException | |
248 | * in case of error | |
249 | */ | |
250 | private List<String> doGetCard(String name) throws IOException { | |
251 | List<String> lines = new LinkedList<String>(); | |
252 | ||
253 | if (name != null && name.length() > 0) { | |
254 | File vcf = new File(dataDir.getAbsolutePath() + File.separator | |
255 | + name); | |
256 | ||
257 | if (vcf.exists()) { | |
258 | Card card = new Card(vcf, Format.VCard21); | |
259 | ||
260 | // timestamp: | |
261 | lines.add(StringUtils.fromTime(card.getLastModified())); | |
262 | ||
263 | // TODO: !!! fix this !!! | |
264 | for (String line : card.toString(Format.VCard21).split("\r\n")) { | |
265 | lines.add(line); | |
266 | } | |
267 | } | |
268 | } | |
269 | ||
270 | return lines; | |
271 | } | |
272 | ||
273 | /** | |
274 | * Save the data to the new given resource. | |
275 | * | |
276 | * @param name | |
277 | * the resource name to save | |
278 | * @param data | |
279 | * the data to save | |
280 | * | |
281 | * @throws IOException | |
282 | * in case of error | |
283 | */ | |
284 | private void doPostCard(String name, List<String> data) throws IOException { | |
285 | if (name != null && name.length() > 0) { | |
286 | File vcf = new File(dataDir.getAbsolutePath() + File.separator | |
287 | + name); | |
288 | ||
289 | Card card = new Card(Parser.parse(data, Format.VCard21)); | |
290 | card.saveAs(vcf, Format.VCard21); | |
291 | } | |
292 | } | |
293 | } |