OSDN Git Service

HTML修正♪
[wordring-tm/wordring-tm.git] / proxy / tmhttp.cpp
1 #include "tmhttp.h"
2 #include "html.h"
3 #include "htmlprivate.h"
4 #include "tmservice.h"
5 #include "settings.h"
6
7 #include <QThread>
8
9 #include <QNetworkAccessManager>
10 #include <QNetworkCookieJar>
11 #include <QNetworkRequest>
12 #include <QNetworkReply>
13
14 #include <QFile>
15
16 #include "debug.h"
17
18 // ProxyContext ---------------------------------------------------------------
19
20 TM::ProxyContext::ProxyContext(Settings *settings, quint16 http, quint16 socket)
21         : QObject(0)
22         , m_settings(settings)
23         , m_manager(new QNetworkAccessManager(this))
24         , m_cookie(new QNetworkCookieJar(this))
25         , m_http_port(http)
26         , m_socket_port(socket)
27 {
28         m_manager->setCookieJar(m_cookie);
29         assert(settings->contains("ProxyModule/prefix"));
30         m_prefix = settings->value("ProxyModule/prefix").toString();
31         assert(settings->contains("ProxyHandler/jscode"));
32         m_jscode = settings->value("ProxyHandler/jscode").toString();
33 }
34
35 QNetworkAccessManager* TM::ProxyContext::network_access_manager()
36 {
37         return m_manager;
38 }
39
40 quint16 TM::ProxyContext::http_port() const { return m_http_port; }
41
42 quint16 TM::ProxyContext::socket_port() const { return m_socket_port; }
43
44 QString TM::ProxyContext::prefix() const { return m_prefix; }
45
46 QString TM::ProxyContext::jscode() const { return m_jscode; }
47
48 // ProxyModule ----------------------------------------------------------------
49
50 TM::ProxyModule::ProxyModule(Settings *settings, quint16 http, quint16 socket,
51                                                         QObject *parent)
52         : HttpModule(parent)
53         , m_thread(new QThread(this))
54         , m_context(new ProxyContext(settings, http, socket))
55 {
56         m_context->moveToThread(m_thread);
57         connect(m_thread, SIGNAL(finished()), m_context, SLOT(deleteLater()));
58
59         m_thread->start();
60 }
61
62 TM::ProxyModule::~ProxyModule()
63 {
64         m_thread->quit();
65         m_thread->wait();
66 }
67
68 HttpHandler* TM::ProxyModule::create_handler(QByteArray method, QByteArray url)
69 {
70         if(method.toLower() != "get") return nullptr;
71         if(!url.toLower().startsWith(m_context->prefix().toUtf8())) return nullptr;
72
73         HttpHandler *handler = new ProxyHandler(m_context);
74         handler->moveToThread(m_thread);
75         return handler;
76 }
77
78 void TM::ProxyModule::destroy_handler(HttpHandler *handler)
79 {
80         QMetaObject::invokeMethod(handler, "deleteLater");
81 }
82
83 // ProxyHandler ---------------------------------------------------------------
84
85 TM::ProxyHandler::ProxyHandler(ProxyContext *context)
86         : HttpHandler(0)
87         , m_context(context)
88         , m_reply(nullptr)
89 {
90 }
91
92 TM::ProxyHandler::~ProxyHandler()
93 {
94         if(m_reply) m_reply->deleteLater();
95 }
96
97 int TM::ProxyHandler::run()
98 {
99         m_targetUrl = QUrl::fromUserInput(
100                         m_request->url().remove(0, m_context->prefix().toUtf8().length()));
101         // HTML取得要求
102         m_reply = m_context->network_access_manager()->get(QNetworkRequest(m_targetUrl));
103         connect(m_reply, SIGNAL(finished()), this, SLOT(onNetworkReplyFinished()));
104         return 0;
105 }
106
107 void TM::ProxyHandler::onNetworkReplyFinished()
108 {
109         assert(m_reply);
110
111         // HTMLがやってきた
112         int status = m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
113
114         switch(status)
115         {
116         case 200:
117                 response(); return;
118         case 301: // リダイレクト先をブラウザにキャッシュさせないため302に固定。
119         case 302:
120                 redirect(); return;
121         case 502:
122                 code_502(); return;
123         }
124         code_404();
125 }
126
127 /*!
128  * \brief ブラウザに302を返しリダイレクトさせます。
129  * \return 未使用。
130  */
131 int TM::ProxyHandler::redirect()
132 {
133         assert(m_reply);
134         QUrl url = m_reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
135         if(url.scheme() != "http") return code_502();
136
137         url.setScheme("");
138         QString redirect =
139                 host() + prefix() +  url.toString().remove(0, 2); // スキーム(http:)を除いた後の//を取り除く
140
141         return code_302(QUrl(redirect));
142 }
143
144 /*!
145  * \brief 出力を開始します。
146  * \return 未使用。
147  */
148 int TM::ProxyHandler::response()
149 {
150         assert(m_reply);
151
152         QString output;
153         QByteArray bytes = m_reply->readAll();
154         if(bytes.isEmpty()) return code_502();
155
156         HtmlDocument document(bytes);
157         if(!document) return code_502();
158
159         HtmlNode html = document.first("html");
160         HtmlNode head = html.first("head");
161         HtmlNode body = html.first("body");
162         HtmlNode node;
163
164         // Tidyが書き込むmetaタグを除去
165         head.remove(head.first("meta"));
166         // ノードを巡回しきれいにする
167         for(Html::pointer p = html.lbegin(); p; p = sanitize(p));
168         // baseを設定(元のHTMLに設定されている場合、書き換えない)
169         HtmlNode base = head.first("base");
170         if(base.type() == HtmlNode::Null)
171         {
172                 base = head.insert("base", head.begin());
173                 base.set_attribute("href", m_targetUrl.toString());
174         }
175         // CSS書き込み
176         node = head.insert("style", head.end());
177         node.insert_text(" .wordring-segment:hover{ color:#ff0000; } ", node.end());
178         // JS書き込み
179
180         QFile file(m_context->jscode());
181         file.open(QIODevice::ReadOnly);
182         QByteArray js = file.readAll();
183         file.close();
184         node = head.insert("script", head.end()).set_attribute("type", "text/javascript");
185         node.insert_comment(QString::fromUtf8(js), node.end());
186         //
187         node = head.insert("script", head.end()).set_attribute("type", "text/javascript");
188         QString str1 = "\nwindow.wordring.port=";
189         str1 += QString::number(m_context->socket_port()) + ";\n";
190         str1 += "window.wordring.url='";
191         str1 += m_targetUrl.toString() + "';\n";
192         node.insert_comment(str1, node.end());
193         // パラグラフ設定
194         create_paragraph(body.lbegin(), body.ltail());
195         // ドキュメント書き出し
196         m_response.set_attribute("Content-Type", "text/html");
197         m_response.set_attribute("Transfer-Encoding", "chunked");
198         write_header();
199         write(document.to_byte_array());
200         write(output.toUtf8());
201
202         finish();
203         return 0;
204 }
205
206 /*!
207  * \brief 与えられたノードpを綺麗にします。
208  * \param p ノードの先頭。
209  * \return 次のノード。
210  *
211  * - script要素を除去します。
212  * - noscript要素をunwrapします。
213  * - hrefを書き換えます。
214  */
215 Html::pointer TM::ProxyHandler::sanitize(Html::pointer p)
216 {
217         bool remove_ = false;
218         Html::pointer result = p->lnext();
219         HtmlNode n(p);
220         if(p->type() == Html::Element && p->place() == Html::Open)
221         {
222                 int tclass_ = p->tclass();
223
224                 if(tclass_ == Html::Script) remove_ = true;
225                 else if(tclass_ == Html::Noscript)
226                 {
227                         HtmlRange r(p, p->ltail());
228                         r.unwrap();
229                 }
230                 else
231                 {
232                         if(tclass_ == Html::A)
233                         {
234                                 QString href = p->attribute("href");
235                                 if(!href.isEmpty()) p->set_attribute("href", create_href(href));
236                         }
237
238                 }
239         }
240
241         if(remove_)
242         {
243                 result = p->ltail()->lnext();
244                 p->lparent()->lremove(p->lbegin(), p->ltail());
245         }
246         return result;
247 }
248
249 /*!
250  * \brief Aタグのhreh属性を都合よく作り直して返します。
251  * \param href 元のhref。
252  * \return 都合の良いhref。
253  */
254 QString TM::ProxyHandler::create_href(QString href)
255 {
256         if(href.startsWith("mailto:"));
257         else if(href.startsWith("https:"));
258         else if(href.startsWith("ftp:"));
259         else if(href.startsWith("http://"))
260                 href.remove(0, 7).insert(0, host() + prefix());
261         else if(href.startsWith("//"))
262                 href.remove(0, 2).insert(0, host() + prefix());
263         else if(href.startsWith("/"))
264                 href = host() + prefix() + m_reply->url().host() + href;
265         else href = host() + prefix()
266                                 + m_reply->url().resolved(QUrl(href)).toString().remove(0, 7);
267         return href;
268 }
269
270 /*!
271  * \brief パラグラフと見做せる範囲を探し、SPANで囲みます。
272  * \param begin_ 探索範囲の始点。
273  * \param tail_ 探索範囲の終点。
274  */
275 void TM::ProxyHandler::create_paragraph(Html::pointer begin_, Html::pointer tail_)
276 {
277         HtmlRange range;
278         int count = 0;
279         Html::pointer skipper;
280         int state = 0;
281         Html::pointer p = begin_, p1, p2;
282         while(p)
283         {
284                 switch(state)
285                 {
286                 case 0:
287                         if(p->type() == Html::Text && !(p->tinfo() & Html::Whitespace)) {
288                                 state = 1;
289                                 p1 = p2 = p;
290                         }
291                         else if(p->tcategory() & Html::Ignore) {
292                                 state = 2;
293                                 skipper = p;
294                         }
295                         break;
296                 case 1:
297                         if(p->type() == Html::Text && !(p->tinfo() & Html::Whitespace)) {
298                                 p2 = p;
299                                 break;
300                         }
301                         else if(p->tcategory() & Html::Ignore) {
302                                 state = 2;
303                                 skipper = p;
304                         }
305                         else if(p->tcategory() & Html::Splitter) state = 0;
306                         else break;
307                         range = HtmlRange(p1, p2);
308                         range.expand();
309                         range.wrap("span")->set_attribute(
310                                 "data-wordring-segment", QString::number(count++)).set_attribute(
311                                 "class", "wordring-segment");
312                         break;
313                 case 2:
314                         if(p == skipper->ltail()) state = 0;
315                         break;
316                 }
317                 p = p->lnext();
318         }
319 }
320
321 /*!
322  * \brief ブラウザとの通信に使うスキーム、ホスト名、ポート番号までを返します。
323  * \return
324  *
325  * 例)http://localhost:80
326  */
327 QString TM::ProxyHandler::host() const
328 {
329         return  QString("http://localhost:") + QString::number(m_context->http_port());
330 }
331
332 /*!
333  * \brief ブラウザとの通信に使う接頭辞を返します。
334  * \return
335  *
336  * 例)/tm?
337  */
338 QString TM::ProxyHandler::prefix() const { return m_context->prefix(); }
339
340 // DefaultHtmlModule -----------------------------------------------------------------
341
342 TM::DefaultHtmlModule::DefaultHtmlModule(Settings *settings, quint16 port, QObject *parent)
343         : HttpModule(parent)
344         , m_settings(settings)
345         , m_port(port)
346 {
347         QString path = settings->value("DefaultHtmlModule/file").toString();
348         QString prefix = settings->value("ProxyModule/prefix").toString();
349
350         QFile file(path);
351         file.open(QIODevice::ReadOnly);
352         QByteArray ba = file.readAll();
353         file.close();
354
355         HtmlDocument document(ba);
356         assert(document);
357
358         HtmlNode html = document.first("html");
359         HtmlNode head = html.first("head");
360         HtmlNode node;
361
362         // Tidyが書き込むmetaタグを除去
363         head.remove(head.first("meta"));
364
365         node = head.insert("script", head.end()).set_attribute("type", "text/javascript");
366         QString js = "\nwindow.wordring.port=";
367         js += QString::number(port) + ";\n";
368         js += "window.wordring.prefix='";
369         js += prefix + "';\n";
370         node.insert_comment(js, node.end());
371
372         m_html = document.to_byte_array();
373 }
374
375 TM::DefaultHtmlModule::~DefaultHtmlModule()
376 {
377 }
378
379 HttpHandler* TM::DefaultHtmlModule::create_handler(QByteArray method, QByteArray url)
380 {
381         if(method.toLower() != "get") return nullptr;
382         if(url != "/") return nullptr;
383
384         HttpHandler *handler = new DefaultHtmlHandler(m_settings, m_port, m_html, this);
385         return handler;
386 }
387
388 void TM::DefaultHtmlModule::destroy_handler(HttpHandler *handler)
389 {
390         delete handler;
391 }
392
393 // DefaultHtmlHandler ----------------------------------------------------------------
394
395 TM::DefaultHtmlHandler::DefaultHtmlHandler(
396                 Settings *settings, quint16 port, QByteArray html_, DefaultHtmlModule *module)
397         : HttpHandler(module)
398         , m_settings(settings)
399         , m_port(port)
400         , m_html(html_)
401 {
402         QString path = settings->value("DefaultHtmlModule/file").toString();
403         QString prefix = settings->value("ProxyModule/prefix").toString();
404
405         QFile file(path);
406         file.open(QIODevice::ReadOnly);
407         QByteArray ba = file.readAll();
408         file.close();
409
410         HtmlDocument document(ba);
411         assert(document);
412
413         HtmlNode html = document.first("html");
414         HtmlNode head = html.first("head");
415         HtmlNode node;
416
417         // Tidyが書き込むmetaタグを除去
418         head.remove(head.first("meta"));
419
420 /*
421         node = head.insert("script", head.end()).set_attribute("type", "text/javascript");
422         QString js = "\nwindow.wordring.port=";
423         js += QString::number(port) + ";\n";
424         js += "window.wordring.prefix='";
425         js += prefix + "';\n";
426         node.insert_comment(js, node.end());
427 */
428
429         m_html = document.to_byte_array();
430 }
431
432 TM::DefaultHtmlHandler::~DefaultHtmlHandler()
433 {
434
435 }
436
437 int TM::DefaultHtmlHandler::run()
438 {
439         m_response.set_attribute("Content-Type", "text/html");
440         m_response.set_attribute("Transfer-Encoding", "chunked");
441         write_header();
442         write(m_html);
443
444         finish();
445         return 0;
446
447 }
448
449
450
451
452