OSDN Git Service

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