1 // Copyright (C) 2014, 2015 Kazuhiro Fujieda <fujieda@users.osdn.me>
\r
3 // Licensed under the Apache License, Version 2.0 (the "License");
\r
4 // you may not use this file except in compliance with the License.
\r
5 // You may obtain a copy of the License at
\r
7 // http://www.apache.org/licenses/LICENSE-2.0
\r
9 // Unless required by applicable law or agreed to in writing, software
\r
10 // distributed under the License is distributed on an "AS IS" BASIS,
\r
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
\r
12 // See the License for the specific language governing permissions and
\r
13 // limitations under the License.
\r
16 using System.Collections.Generic;
\r
17 using System.Globalization;
\r
20 using System.Net.Sockets;
\r
22 using System.Text.RegularExpressions;
\r
24 namespace KancolleSniffer
\r
26 public class LogServer
\r
28 private static readonly string IndexDir = AppDomain.CurrentDomain.BaseDirectory;
\r
29 private static string _outputDir = AppDomain.CurrentDomain.BaseDirectory;
\r
31 public static string OutputDir
\r
33 set => _outputDir = value;
\r
36 public static MaterialCount[] MaterialHistory { private get; set; }
\r
38 public static void Process(Socket client, string requestLine)
\r
40 var from = DateTime.MinValue;
\r
41 var to = DateTime.MaxValue;
\r
42 var timestamp = false;
\r
44 var request = requestLine.Split(' ');
\r
45 if (request.Length != 3)
\r
47 SendError(client, "400 Bad Request");
\r
50 if (!request[0].StartsWith("GET", StringComparison.OrdinalIgnoreCase))
\r
52 SendError(client, "501 Not Implemented");
\r
55 var tmp = request[1].Split('?');
\r
56 var path = HttpUtility.UrlDecode(tmp[0]);
\r
57 if (path == null || !path.StartsWith("/"))
\r
59 SendError(client, "400 Bad Request");
\r
62 if (tmp.Length == 2)
\r
64 var query = HttpUtility.ParseQueryString(tmp[1]);
\r
65 if (query["from"] != null)
\r
67 double.TryParse(query["from"], out var tick);
\r
68 from = new DateTime(1970, 1, 1).ToLocalTime().AddSeconds(tick / 1000);
\r
70 if (query["to"] != null)
\r
72 double.TryParse(query["to"], out var tick);
\r
73 to = new DateTime(1970, 1, 1).ToLocalTime().AddSeconds(tick / 1000);
\r
75 if (query["number"] != null)
\r
76 timestamp = query["number"] == "true";
\r
79 path = path == "/" ? "index.html" : path.Substring(1);
\r
80 var full = Path.Combine(IndexDir, path);
\r
81 var csv = Path.Combine(_outputDir, path);
\r
82 if (path.EndsWith(".html", StringComparison.OrdinalIgnoreCase) && File.Exists(full))
\r
84 SendFile(client, full, "text/html");
\r
87 if (path.EndsWith(".csv", StringComparison.OrdinalIgnoreCase) && File.Exists(csv))
\r
89 SendFile(client, csv, "text/csv; charset=Shift_JIS");
\r
92 if (path.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
\r
94 SendJsonData(client, csv, from, to, timestamp);
\r
97 if (path.EndsWith(".js", StringComparison.OrdinalIgnoreCase) && File.Exists(full))
\r
99 SendFile(client, full, "application/javascript");
\r
102 if (path.EndsWith(".pac"))
\r
104 SendProxyPac(client, HttpProxy.LocalPort);
\r
107 if (File.Exists(full))
\r
109 SendFile(client, full, "text/plain");
\r
112 SendError(client, "404 Not Found");
\r
115 private static void SendError(Socket client, string error)
\r
117 using (var writer = new StreamWriter(new MemoryStream(), Encoding.ASCII))
\r
119 writer.Write("HTTP/1.1 {0}\r\n", error);
\r
120 writer.Write("Server: KancolleSniffer\r\n");
\r
121 writer.Write("Date: {0:R}\r\n", DateTime.Now);
\r
122 writer.Write("Connection: close\r\n\r\n");
\r
123 writer.Write("<html><head><title>{0}</title></head>\r\n", error);
\r
124 writer.Write("<body><h4>{0}</h4></body></html>\r\n\r\n", error);
\r
126 client.Send(((MemoryStream)writer.BaseStream).ToArray());
\r
130 private static void SendJsonData(Socket client, string path, DateTime from, DateTime to, bool number)
\r
132 using (var header = new StreamWriter(new MemoryStream(), Encoding.ASCII))
\r
134 header.Write("HTTP/1.1 200 OK\r\n");
\r
135 header.Write("Server: KancolleSniffer\r\n");
\r
136 header.Write("Date: {0:R}\r\n", DateTime.Now);
\r
137 header.Write("Content-Type: {0}\r\n", "application/json; charset=Shift_JIS");
\r
138 header.Write("Connection: close\r\n\r\n");
\r
140 client.Send(((MemoryStream)header.BaseStream).ToArray());
\r
142 var csv = path.Replace(".json", ".csv");
\r
143 var encoding = Encoding.GetEncoding("Shift_JIS");
\r
144 client.Send(encoding.GetBytes("{ \"data\": [\n"));
\r
145 var battle = false;
\r
146 var material = false;
\r
149 if (!File.Exists(csv))
\r
152 if (path.EndsWith("遠征報告書.json"))
\r
156 else if (path.EndsWith("改修報告書.json"))
\r
160 else if (path.EndsWith("海戦・ドロップ報告書.json"))
\r
165 else if (path.EndsWith("開発報告書.json"))
\r
169 else if (path.EndsWith("建造報告書.json"))
\r
173 else if (path.EndsWith("資材ログ.json"))
\r
178 else if (path.EndsWith("戦果.json"))
\r
182 var delimiter = "";
\r
183 foreach (var line in File.ReadLines(csv, encoding).Skip(1))
\r
185 var data = line.Split(',');
\r
186 if (!DateTime.TryParseExact(data[0], Logger.DateTimeFormat, CultureInfo.InvariantCulture,
\r
187 DateTimeStyles.AssumeLocal, out DateTime date))
\r
189 // システムが和暦に設定されていて和暦が出力されてしまったケースを救う
\r
190 var wareki = CultureInfo.CreateSpecificCulture("ja-JP");
\r
191 wareki.DateTimeFormat.Calendar = new JapaneseCalendar();
\r
192 if (DateTime.TryParseExact(data[0], Logger.DateTimeFormat, wareki,
\r
193 DateTimeStyles.AssumeLocal, out date))
\r
195 data[0] = Logger.FormatDateTime(date);
\r
197 else if (DateTime.TryParse(data[0], CultureInfo.CurrentCulture,
\r
198 DateTimeStyles.AssumeLocal, out date))
\r
200 data[0] = Logger.FormatDateTime(date);
\r
207 if (date < from || to < date)
\r
209 IEnumerable<string> entries = data;
\r
211 entries = data.Take(9);
\r
213 entries = BattleLogProcessor.Process(data);
\r
214 if (entries.Count() != records)
\r
218 var stamp = ((date.ToUniversalTime().Ticks -
\r
219 new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks) /
\r
220 TimeSpan.TicksPerMillisecond).ToString();
\r
221 client.Send(encoding.GetBytes(delimiter + "[" + stamp + "," +
\r
222 string.Join(",", entries.Skip(1)) + "]"));
\r
226 client.Send(encoding.GetBytes(delimiter + "[\"" +
\r
227 string.Join("\",\"", entries) + "\"]"));
\r
231 if (material && !number)
\r
233 client.Send(encoding.GetBytes(delimiter + "[\"" +
\r
234 string.Join("\",\"", GetCurrentMaterialRecord()) + "\"]"));
\r
239 client.Send(encoding.GetBytes("]}\n"));
\r
243 private static IEnumerable<string> GetCurrentMaterialRecord()
\r
245 return new[] {Logger.FormatDateTime(DateTime.Now)}.Concat(MaterialHistory.Select(c => c.Now.ToString()));
\r
248 private static void SendFile(Socket client, string path, string mime)
\r
250 using (var header = new StreamWriter(new MemoryStream(), Encoding.ASCII))
\r
252 header.Write("HTTP/1.1 200 OK\r\n");
\r
253 header.Write("Server: KancolleSniffer\r\n");
\r
254 header.Write("Date: {0:R}\r\n", DateTime.Now);
\r
255 header.Write("Content-Length: {0}\r\n", new FileInfo(path).Length);
\r
256 header.Write("Content-Type: {0}\r\n", mime);
\r
257 header.Write("Connection: close\r\n\r\n");
\r
259 client.SendFile(path, ((MemoryStream)header.BaseStream).ToArray(), null,
\r
260 TransmitFileOptions.UseDefaultWorkerThread);
\r
264 private static void SendProxyPac(Socket client, int port)
\r
266 using (var header = new StreamWriter(new MemoryStream(), Encoding.ASCII))
\r
268 header.Write("HTTP/1.1 200 OK\r\n");
\r
269 header.Write("Server: KancolleSniffer\r\n");
\r
270 header.Write("Date: {0:R}\r\n", DateTime.Now);
\r
271 header.Write("Content-Type: application/x-ns-proxy-autoconfig\r\n");
\r
272 header.Write("Connection: close\r\n\r\n");
\r
274 client.Send(((MemoryStream)header.BaseStream).ToArray());
\r
279 pacFile = File.ReadAllText("proxy.pac").Replace("8080", port.ToString());
\r
285 client.Send(Encoding.ASCII.GetBytes(pacFile));
\r
289 public static class BattleLogProcessor
\r
291 public static IEnumerable<string> Process(string[] data)
\r
293 if (data.Length == 35)
\r
294 data = data.Concat(Enumerable.Repeat("", 3)).ToArray();
\r
295 if (data.Length == 40)
\r
297 data = data.Take(21).Concat(new[] {data[21] + "・" + data[23], data[22] + "・" + data[24]})
\r
298 .Concat(data.Skip(25)).ToArray();
\r
300 else if (data.Length != 38)
\r
304 if (data[5] == "T字戦(有利)")
\r
306 if (data[5] == "T字戦(不利)")
\r
308 if (data[6].EndsWith("航行序列"))
\r
309 data[6] = data[6].Substring(0, 4);
\r
310 if (data[7].EndsWith("航行序列"))
\r
311 data[7] = data[7].Substring(0, 4);
\r
312 data[37] = ShortenAirBattleResult(data[37]);
\r
313 return AddDamagedShip(data);
\r
316 private static string ShortenAirBattleResult(string result)
\r
335 private static IEnumerable<string> AddDamagedShip(string[] data)
\r
337 var damaged = new List<string>();
\r
338 for (var i = 11; i < 11 + 12; i += 2)
\r
342 var ship = data[i] = StripKana(data[i]);
\r
343 var hp = data[i + 1];
\r
346 damaged.AddRange(from entry in ship.Split('・').Zip(hp.Split('・'), (s, h) => new {s, h})
\r
347 where entry.h.Contains("/")
\r
348 let nm = entry.h.Split('/').Select(int.Parse).ToArray()
\r
349 where ShipStatus.CalcDamage(nm[0], nm[1]) == ShipStatus.Damage.Badly
\r
352 catch (FormatException)
\r
357 return data.Take(23).Concat(new[] {string.Join("・", damaged)}).Concat(data.Skip(23));
\r
360 private static readonly Regex Kana = new Regex(@"\([^)]+\)\(", RegexOptions.Compiled);
\r
362 private static string StripKana(string name)
\r
364 return Kana.Replace(name, "(");
\r