Socketを使ったHTTPサーバーとHTTPクライアント(+Webブラウザー) - Python3

![Untitled.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/448552/ec511fb0-bf4a-8585-72e1-634817c8e146.jpeg)


# Socketモジュールを使ったHTTP通信

python3のsocketモジュールを使った、HTTP通信の簡単な例

HTTPは簡単に言うと、HTMLファイルをTCP通信(TCPとは限らない)で送ったりするために、本来はどんなデータでも良いTCPのデータの部分(TCPペイロードとか言う)に対してルール(データはこのフォーマットに従ってくださいという)を作ったわけだから、recv,sendでそのフォーマットに従ったデータを送れば良い。

ここでは、[RFC7230](https://tools.ietf.org/html/rfc7230),[RFC7230日本語訳](https://help.amanohina.com/RFC7230-ja.html)を読みながら進める。

![TCPandHTTP.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/448552/28555044-644d-05bf-b61d-8639c00d6084.jpeg)

![http通信.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/448552/1b7f04cb-d6b7-55b5-e77a-513a4228d842.jpeg)

※HTTP通信とは[「HTTP」と呼ばれるお約束事に従って行われる通信のこと。](https://wa3.i-3-i.info/word167.html)だそうです。

##スタートライン(start-line)とヘッダ節(header-section)

### スタートラインについて start-line

HTTPメッセージはクライアントからサーバーへのリクエストとサーバーからクライアントへのレスポンスのいずれかになる。このスタートラインでこの２つを判別することができる。スタートラインは前者の場合[リクエストライン](https://help.amanohina.com/RFC7230-ja.html#p.request-line)、後者の場合[ステータスライン](https://help.amanohina.com/RFC7230-ja.html#p.status-line)というフォーマットを取る。

###ヘッダ節について header-section

今回は次のヘッダフィールドを使う。

#### Serverサイド

1. Server
2. Connection
3. Content-Type
4. Content-Length

####Clientサイド

1. Accept
2. Host
3. Connection
4. User-Agent

実際のところ今回はどのヘッダフィールドの値も使いませんでした。

### ここまでの実装

ここではコードやヘッダ節の値を固定しているが、実際には知っての通り動的に決められます。

#### クライアント側のリクエストライン＋ヘッダ節

```Python3
RETURN_PHRASE = '\r\n'
VERSION = 'HTTP/1.1'


def make_request_line(mtd, request_target):
    '''Return http_request-line'''

    request_line = \
        mtd + ' ' + request_target + ' ' + VERSION + RETURN_PHRASE
    return request_line


def make_header_section():
    '''Return http_header-section'''

    field_values = {
        'Host': '192.168.1.200',
        'Accept': 'html/plain',
        'Connection': 'close',
        'User-Agent': 'Suzukaze Browser 1.0'
    }
    header_sc = 'Host:' + field_values['Host'] + RETURN_PHRASE
    header_sc += 'Accept:' + field_values['Accept'] + RETURN_PHRASE
    header_sc += 'Connection:' + field_values['Connection'] + RETURN_PHRASE
    header_sc += 'User-Agent:' + field_values['User-Agent'] + RETURN_PHRASE
    header_sc += RETURN_PHRASE
    return header_sc


mtd = 'GET'
request_target = '/'
request_line = make_request_line(mtd, request_target)

header_section = make_header_section()

print(header_section)
```

関数`make_request_line(メソッド, リクエストターゲット) -> リクエストライン`は次を返す。

```
GET / HTTP/1.1
```

関数`make_header_section() -> ヘッダ節`は次を返す。

```
Host:192.168.1.200
Accept:html/plain
Connection:close
User-Agent:Suzukaze Browser 1.0
空行
```

#### サーバー側のステータスライン＋ヘッダ節

コードは200固定。だから`reason_phrases`は余計だが雰囲気のために追加。

```
RETURN_PHRASE = '\r\n'
VERSION = 'HTTP/1.1'


def make_status_line():
    '''Return http_staus_line'''

    reason_phrases = {
        '200': 'OK',
        '403': 'Forbidden',
        '404': 'Not Found',
        '405': 'Method Not Allowed'
    }
    status_line = \
        VERSION + ' ' + '200' + ' ' + reason_phrases['200'] + RETURN_PHRASE
    return status_line


def make_header_section(content_length):
    '''Return http_header_section'''

    field_values = {
        'Server': 'Suzukaze Server 1.0',
        'Date': datetime.now(timezone.utc).strftime(r'%a, %d %b %Y %X %Z'),
        'Connection': 'close',
        'Content-Type': 'html/plain',
        'Content-Length': content_length
    }
    header_sc = 'Date:' + field_values['Date'] + RETURN_PHRASE
    header_sc += 'Server:' + field_values['Server'] + RETURN_PHRASE
    header_sc += 'Connection' + field_values['Connection'] + RETURN_PHRASE
    header_sc += 'Content-Type' + field_values['Content-Type'] + RETURN_PHRASE
    header_sc += 'Content-Length' + field_values['Cotent-Length'] + RETURN_PHRASE
    header_sc += RETURN_PHRASE
    return header_sc


def read_index_html():
    '''Return index.html and that length'''

    with open('index.html', 'rb') as f:
        index_html = f.read()
        index_len = len(index_html)
        return index_html, index_len


status_line = make_status_line()

index_html, index_len = read_index_html()
content_length = index_len

header_section = make_header_section(content_length)
```

#####関数`make_status_line() -> ステータスライン`は次を返す。

```
HTTP/1.1 200 OK
```

#####関数`make_header_section(Content-Lengthの値) -> ヘッダ節`は次を返す。

```
Date:Mon, 00 Nov 2020 00:00:00 UTC
Server:Suzukaze Server 1.0
Connection:close
Content-Type:html/plain
Content-Length:1224
空行
```

#####関数`read_index_html() -> index.htmlの中身, index.htmlの長さ`は次を返す。

```Python3
b"<!DOCTYPE html>\n<head>\n    <meta charset='utf-8'>\n</head>\n<style>\n* 
(略)
ab\xe3\x81\xa4\xe3\x81\x84\xe3\x81\xa6</li>\n                <li>\xe5\x95\x8f\xe3\x81\x84\xe5\x90\x88\xe3\x82\x8f\xe3\x81\x9b</li>\n            </ul>\n        </footer>\n    </div>\n</body>\n"
```

```
1224
```

### サーバー側がリクエストから取り出す必要がある情報

今回のコードではリクエストされるのが'/'であることがわかっているので、request-targetは必要ないし、ConnectionもCloseするから結局のところサーバーが必要な情報はありません。だからリクエストラインとヘッダ節はスルーします。

##メッセージボディ(message-body)

サーバー側のメッセージボディを実装すると言ってもすでにindex.htmlをバイト列として読み込んだのでサーバー側のヘッダ節の後ろにくっつけるだけである。クライアント側のメッセージボディは無い。

ここで注意しないといけないのがスタートラインとヘッダ節がUnicode文字列(str型)であることである。ソケットで送信するにはバイト列(byte-like-object)にする必要があるので変換する。

#### クライアント側

```Python3
mtd = 'GET'
request_target = '/'
request_line = make_request_line(mtd, request_target)

header_section = make_header_section()

http_msg = \
    bytes(request_line, 'utf-8') + bytes(request_line, 'utf-8')
```

#### サーバー側

```Python3
status_line = make_status_line()

index_html, index_len = read_index_html()
msg_body = index_html
content_length = str(index_len)

header_section = make_header_section(content_length)

http_msg = \
    bytes(status_line, 'utf-8') + bytes(header_section, 'utf-8') + msg_body
```

## ソケット通信の実装

後はこのHTTPメッセージを送り合うだけです。

ここのコードは、[Socketモジュールを使ったTCP通信 - Python3](https://qiita.com/Suzukaze31/items/0a342a29014b7c37c2fc)をベースに作ります。

#### サーバー側

```Python3
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(SERVER_ADDRESS)
sock.listen(0)

try:
    conn_sock, client_address = sock.accept()
    print_msg('ac', 'The connection accepted.')
    print_msg('i', '{}:{} --------> {}:{}'
              .format(client_address[0], client_address[1],
                      SERVER_ADDRESS[0], SERVER_ADDRESS[1]))

    # Receiving the request
    recd_data = b''
    while True:
        data = conn_sock.recv(32)
        if not data:
            break
        recd_data += data
    print_msg('i', 'Total received: {}'.format(recd_data))

    # Sending the response
    http_msg = make_http_msg()
    remaining = len(http_msg)
    res = 0
    while remaining > 0:
        res = conn_sock.send(http_msg[res:])
        remaining -= res
finally:
    conn_sock.shutdown(socket.SHUT_WR)
    conn_sock.close()
    print_msg('cl', 'The connection closed.')
```

#### クライアント側

リクエストを送り終えたらshutdownすることでサーバー側にこれ以上こちらはデータを送らないと宣言できる。(ソケットレベルでは空バイトが送られる。だからサーバー側は空バイトの受信でリクエストの終端を検出できる。)

```Python3
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

try:
    sock.connect(SERVER_ADDRESS)
    print_msg('cn', 'The connection accepted')

    # Sending the request
    http_msg = make_http_msg()
    remaining = len(make_http_msg())
    res = 0
    while remaining > 0:
        res = sock.send(http_msg[res:])
        remaining -= res

    sock.shutdown(socket.SHUT_WR)

    # Receiving the response
    recd_data = b''
    while True:
        data = sock.recv(32)
        print(data)
        if not data:
            break
        recd_data += data
finally:
    sock.close()
    print_msg('cl', 'The connection closed.')
```

## データを取り出す

####クライアント側

とりあえずクライアント側は次の２つを取り出す必要がある。

1. ステータスラインの情報。これは次の正規表現で辞書として取り出す。

   ```Python3
   STS_LINE_PTN = b'''
   (?P<version>HTTP/[0-9][.][0-9])[ ]
   (?P<status_code>[0-9][0-9][0-9])[ ]
   (?P<reason>([!\"#$%&\'()*+,-./0-9:;<=>?@A-Z[\\]^_`a-z{|}~ ]|\t)+)
   ([\r][\n])
   '''
   ```

   ※コンパイル時に`re.VERBOSE`を指定する必要がある。そうすることで文字クラス`[]`内にない空白は無視される。

   次のように取り出される。

   ```Python3
   {'version': b'HTTP/1.1', 'status_code': b'200', 'reason': b'OK'}
   ```

   

2. htmlのコードを含んだメッセージボディ。ヘッダ節の終わりは空行で表すルールなので、`b'\r\n\r\n'`と一致する部分の最後のインデックスを取得して、そのインデックスを`[start:end]`のstartにして新しい(メッセージボディだけの)バイト列を生成する。そしてそれをデコードすると元のhtmlのコードが得られます。つまりサーバーサイドの下記文字列(html_source)と等しい。

   ```Python3
   with open('sample.html', 'r') as f:
   	html_source = f.read()
   ```

#### サーバー側

今の所サーバー側が必要な情報は無い。

## 得たhtmlをブラウザもどきで表示させる

ここで簡単なhtmlビュワーをPyqt5をつかって作る。UIはデザイナーを使って作り、それを`PyQt5 UI code generator 5.15.1`でPython3コードに変換してそのコードをいじって使う。本来はUIを書き換えても問題ないようにユーザーが追加するロジックは別に作ったクラスに書いていくが今回はUIを書き換えないので、そのまま設定していく。ブラウザは次のようなコードとなった。このモジュールをソケット通信の行うメインのモジュールでインポートする。

```browser_app.py
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWebEngineWidgets import QWebEngineView as QWebView


class Ui_dialog(object):
    def setupUi(self, dialog, html_source):
        dialog.setObjectName('dialog')
        dialog.resize(1000, 650)
        self.stackedWidget = QtWidgets.QStackedWidget(dialog)
        self.stackedWidget.setGeometry(QtCore.QRect(10, 50, 980, 590))
        self.stackedWidget.setObjectName('stackedWidget')
        self.page_1 = QtWidgets.QWidget()
        self.page_1.setObjectName('page_1')
        self.webView = QWebView(self.page_1)
        self.webView.setGeometry(QtCore.QRect(0, 0, 980, 590))
        self.webView.setObjectName('webView')
        self.stackedWidget.addWidget(self.page_1)
        self.page_2 = QtWidgets.QWidget()
        self.page_2.setObjectName('page_2')
        self.page_2_title = QtWidgets.QLabel(self.page_2)
        self.page_2_title.setGeometry(QtCore.QRect(370, 60, 240, 50))
        font = QtGui.QFont()
        font.setPointSize(24)
        self.page_2_title.setFont(font)
        self.page_2_title.setObjectName('page_2_title')
        self.page_2_label1 = QtWidgets.QLabel(self.page_2)
        self.page_2_label1.setGeometry(QtCore.QRect(435, 230, 110, 22))
        font = QtGui.QFont()
        font.setPointSize(16)
        self.page_2_label1.setFont(font)
        self.page_2_label1.setObjectName('page_2_label1')
        self.page_2_ip = QtWidgets.QLineEdit(self.page_2)
        self.page_2_ip.setGeometry(QtCore.QRect(435, 262, 110, 28))
        font = QtGui.QFont()
        font.setPointSize(12)
        self.page_2_ip.setFont(font)
        self.page_2_ip.setObjectName('page_2_ip')
        self.page_2_label2 = QtWidgets.QLabel(self.page_2)
        self.page_2_label2.setGeometry(QtCore.QRect(468, 310, 42, 16))
        font = QtGui.QFont()
        font.setPointSize(16)
        self.page_2_label2.setFont(font)
        self.page_2_label2.setObjectName('page_2_label2')
        self.page_2_url = QtWidgets.QLineEdit(self.page_2)
        self.page_2_url.setGeometry(QtCore.QRect(335, 336, 310, 28))
        font = QtGui.QFont()
        font.setPointSize(12)
        self.page_2_url.setFont(font)
        self.page_2_url.setObjectName('page_2_url')
        self.page_2_save = QtWidgets.QPushButton(self.page_2)
        self.page_2_save.setGeometry(QtCore.QRect(425, 384, 130, 40))
        font = QtGui.QFont()
        font.setPointSize(18)
        self.page_2_save.setFont(font)
        self.page_2_save.setObjectName('page_2_save')
        self.stackedWidget.addWidget(self.page_2)
        self.url_lineEdit = QtWidgets.QLineEdit(dialog)
        self.url_lineEdit.setGeometry(QtCore.QRect(10, 10, 666, 30))
        font = QtGui.QFont()
        font.setPointSize(12)
        self.url_lineEdit.setFont(font)
        self.url_lineEdit.setObjectName('url_lineEdit')
        self.search_btn = QtWidgets.QPushButton(dialog)
        self.search_btn.setGeometry(QtCore.QRect(670, 10, 90, 30))
        font = QtGui.QFont()
        font.setPointSize(12)
        self.search_btn.setFont(font)
        self.search_btn.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
        self.search_btn.setObjectName('search_btn')
        self.config_btn = QtWidgets.QPushButton(dialog)
        self.config_btn.setGeometry(QtCore.QRect(780, 10, 60, 30))
        font = QtGui.QFont()
        font.setPointSize(10)
        self.config_btn.setFont(font)
        self.config_btn.setObjectName('config_btn')

        # ----------------------------------------------

        self.url_lineEdit.setText('準備中')
        self.page_2_title.setText('Setting the DNS')
        self.page_2_label1.setText('IP Address')
        self.page_2_ip.setText('準備中')
        self.page_2_label2.setText('URL')
        self.page_2_url.setText('準備中')

        self.webView.setHtml(html_source)

        # ----------------------------------------------

        QtCore.QMetaObject.connectSlotsByName(dialog)


def mybrowser(html_source):
    app = QtWidgets.QApplication(sys.argv)
    dialog = QtWidgets.QDialog()
    ui = Ui_dialog()
    ui.setupUi(dialog, html_source)
    dialog.show()
    sys.exit(app.exec_())
```

これを同じディレクトリに置いて、クライアント側でインポートして次のように呼び出す。

```
status_dict, res_msg = status_line_parser(recd_data)

if status_dict['status_code'].decode() == '200':
    html_lines = get_msg_body(res_msg)
    html_lines = html_lines.decode()
    mybrowser(html_lines)
```

そうすると、

![Untitled.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/448552/3c5ab755-490e-094e-df2e-8c0fcf4cf77b.jpeg)

このように表示される。

##ソースコード

####client.py

```client.py
import socket
import re
from colored_print import print_msg
from browser_app import mybrowser


# Request


SERVER_ADDRESS = ('192.168.1.201', 8000)
RETURN_PHRASE = '\r\n'
VERSION = 'HTTP/1.1'


def make_request_line(mtd, request_target):
    '''Return http_request-line'''

    request_line = \
        mtd + ' ' + request_target + ' ' + VERSION + RETURN_PHRASE
    return request_line


def make_header_section():
    '''Return http_header-section'''

    field_values = {
        'Host': '192.168.1.200',
        'Accept': 'html/plain',
        'Connection': 'close',
        'User-Agent': 'Suzukaze-Browser-1.0'
    }
    header_sc = 'Host:' + field_values['Host'] + RETURN_PHRASE
    header_sc += 'Accept:' + field_values['Accept'] + RETURN_PHRASE
    header_sc += 'Connection:' + field_values['Connection'] + RETURN_PHRASE
    header_sc += 'User-Agent:' + field_values['User-Agent'] + RETURN_PHRASE
    header_sc += RETURN_PHRASE
    return header_sc


def make_http_msg():
    '''Return http-message'''

    mtd = 'GET'
    request_target = '/'
    request_line = make_request_line(mtd, request_target)

    header_section = make_header_section()

    http_msg = \
        bytes(request_line, 'utf-8') + bytes(header_section, 'utf-8')
    return http_msg


# Response


def status_line_parser(msg_lines):
    '''Return dict of status line and http_msg except status line'''

    STS_LINE_PTN = b'''
    (?P<version>HTTP/[0-9][.][0-9])[ ]
    (?P<status_code>[0-9][0-9][0-9])[ ]
    (?P<reason>([!\"#$%&\'()*+,-./0-9:;<=>?@A-Z[\\]^_`a-z{|}~ ]|\t)+)
    ([\r][\n])
    '''
    p_sts_line = re.compile(STS_LINE_PTN, re.VERBOSE)

    m_sts_line = p_sts_line.match(msg_lines)
    if m_sts_line is not None:
        sts_line_dict = m_sts_line.groupdict()
        return sts_line_dict, msg_lines[m_sts_line.end():]


def get_msg_body(msg_lines):
    '''Return a message body'''
    
    end = msg_lines.rfind(b'\r\n\r\n')
    print(end)
    if end is not -1:
        return msg_lines[end:]
    else:
        raise ValueError('msg_lines is a invalid format')


########################################################################


sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

try:
    sock.connect(SERVER_ADDRESS)
    print_msg('cn', 'The connection accepted')

    # Sending the request
    http_msg = make_http_msg()
    remaining = len(make_http_msg())
    res = 0
    while remaining > 0:
        res = sock.send(http_msg[res:])
        remaining -= res

    sock.shutdown(socket.SHUT_WR)

    # Receiving the response
    recd_data = b''
    while True:
        data = sock.recv(32)
        print(data)
        if not data:
            break
        recd_data += data
finally:
    sock.close()
    print_msg('cl', 'The connection closed.')


status_dict, res_msg = status_line_parser(recd_data)

if status_dict['status_code'].decode() == '200':
    html_lines = get_msg_body(res_msg)
    html_lines = html_lines.decode()
    mybrowser(html_lines)
```

####server.py

```server.py
import socket
from datetime import datetime, timezone
from colored_print import print_msg


# Response


SERVER_ADDRESS = ('192.168.1.12', 8000)
RETURN_PHRASE = '\r\n'
VERSION = 'HTTP/1.1'


def make_status_line():
    '''Return http_staus-line'''

    reason_phrases = {
        '200': 'OK',
        '403': 'Forbidden',
        '404': 'Not Found',
        '405': 'Method Not Allowed'
    }
    status_line = \
        VERSION + ' ' + '200' + ' ' + reason_phrases['200'] + RETURN_PHRASE
    return status_line


def make_header_section(content_length):
    '''Return http_header-section'''

    field_values = {
        'Server': 'Suzukaze Server 1.0',
        'Date': datetime.now(timezone.utc).strftime(r'%a, %d %b %Y %X %Z'),
        'Connection': 'close',
        'Content-Type': 'html/plain',
        'Content-Length': content_length
    }
    header_sc = 'Date:' + field_values['Date'] + RETURN_PHRASE
    header_sc += 'Server:' + field_values['Server'] + RETURN_PHRASE
    header_sc += 'Connection:' + field_values['Connection'] + RETURN_PHRASE
    header_sc += 'Content-Type:' + field_values['Content-Type'] + RETURN_PHRASE
    header_sc += 'Content-Length:' + field_values['Content-Length'] + RETURN_PHRASE
    header_sc += RETURN_PHRASE
    return header_sc


def read_index_html():
    '''Return index.html and that length'''

    with open('index.html', 'rb') as f:
        index_html = f.read()
        index_len = len(index_html)
        return index_html, index_len


def make_http_msg():
    status_line = make_status_line()

    index_html, index_len = read_index_html()
    msg_body = index_html
    content_length = str(index_len)

    header_section = make_header_section(content_length)

    http_msg = \
        bytes(status_line, 'utf-8') + bytes(header_section, 'utf-8') + msg_body
    return http_msg


###############################################################


sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(SERVER_ADDRESS)
sock.listen(0)

try:
    conn_sock, client_address = sock.accept()
    print_msg('ac', 'The connection accepted.')
    print_msg('i', '{}:{} --------> {}:{}'
              .format(client_address[0], client_address[1],
                      SERVER_ADDRESS[0], SERVER_ADDRESS[1]))

    # Receiving the request
    recd_data = b''
    while True:
        data = conn_sock.recv(32)
        if not data:
            break
        recd_data += data
    print_msg('i', 'Total received: {}'.format(recd_data))

    # Sending the response
    http_msg = make_http_msg()
    remaining = len(http_msg)
    res = 0
    while remaining > 0:
        res = conn_sock.send(http_msg[res:])
        remaining -= res
finally:
    conn_sock.shutdown(socket.SHUT_WR)
    conn_sock.close()
    print_msg('cl', 'The connection closed.')
```

####colored_print.py

```colored_print.py
from colorama import Fore, Style


def print_msg(header, msg):
    '''header are i that is INFO or e that is ERROR'''

    if header == 'i':
        print(Fore.GREEN + '[INFO]',
              Style.RESET_ALL + msg)
    elif header == 'e':
        print(Fore.RED + '[ERROR]',
              Style.RESET_ALL + msg)
    elif header == 'ac':
        print(Fore.BLUE + '[ACCEPT]',
              Style.RESET_ALL + msg)
    elif header == 'cn':
        print(Fore.BLUE + '[CONNECT]',
              Style.RESET_ALL + msg)
    elif header == 'cl':
        print(Fore.BLUE + '[CLOSE]',
              Style.RESET_ALL + msg)
    else:
        print(Fore.RED + 'ERROR: header is an invalid value.'
              + Style.RESET_ALL)
```

####browser_app.py

```browser_app.py
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWebEngineWidgets import QWebEngineView as QWebView


class Ui_dialog(object):
    def setupUi(self, dialog, html_source):
        dialog.setObjectName('dialog')
        dialog.resize(1000, 650)
        self.stackedWidget = QtWidgets.QStackedWidget(dialog)
        self.stackedWidget.setGeometry(QtCore.QRect(10, 50, 980, 590))
        self.stackedWidget.setObjectName('stackedWidget')
        self.page_1 = QtWidgets.QWidget()
        self.page_1.setObjectName('page_1')
        self.webView = QWebView(self.page_1)
        self.webView.setGeometry(QtCore.QRect(0, 0, 980, 590))
        self.webView.setObjectName('webView')
        self.stackedWidget.addWidget(self.page_1)
        self.page_2 = QtWidgets.QWidget()
        self.page_2.setObjectName('page_2')
        self.page_2_title = QtWidgets.QLabel(self.page_2)
        self.page_2_title.setGeometry(QtCore.QRect(370, 60, 240, 50))
        font = QtGui.QFont()
        font.setPointSize(24)
        self.page_2_title.setFont(font)
        self.page_2_title.setObjectName('page_2_title')
        self.page_2_label1 = QtWidgets.QLabel(self.page_2)
        self.page_2_label1.setGeometry(QtCore.QRect(435, 230, 110, 22))
        font = QtGui.QFont()
        font.setPointSize(16)
        self.page_2_label1.setFont(font)
        self.page_2_label1.setObjectName('page_2_label1')
        self.page_2_ip = QtWidgets.QLineEdit(self.page_2)
        self.page_2_ip.setGeometry(QtCore.QRect(435, 262, 110, 28))
        font = QtGui.QFont()
        font.setPointSize(12)
        self.page_2_ip.setFont(font)
        self.page_2_ip.setObjectName('page_2_ip')
        self.page_2_label2 = QtWidgets.QLabel(self.page_2)
        self.page_2_label2.setGeometry(QtCore.QRect(468, 310, 42, 16))
        font = QtGui.QFont()
        font.setPointSize(16)
        self.page_2_label2.setFont(font)
        self.page_2_label2.setObjectName('page_2_label2')
        self.page_2_url = QtWidgets.QLineEdit(self.page_2)
        self.page_2_url.setGeometry(QtCore.QRect(335, 336, 310, 28))
        font = QtGui.QFont()
        font.setPointSize(12)
        self.page_2_url.setFont(font)
        self.page_2_url.setObjectName('page_2_url')
        self.page_2_save = QtWidgets.QPushButton(self.page_2)
        self.page_2_save.setGeometry(QtCore.QRect(425, 384, 130, 40))
        font = QtGui.QFont()
        font.setPointSize(18)
        self.page_2_save.setFont(font)
        self.page_2_save.setObjectName('page_2_save')
        self.stackedWidget.addWidget(self.page_2)
        self.url_lineEdit = QtWidgets.QLineEdit(dialog)
        self.url_lineEdit.setGeometry(QtCore.QRect(10, 10, 666, 30))
        font = QtGui.QFont()
        font.setPointSize(12)
        self.url_lineEdit.setFont(font)
        self.url_lineEdit.setObjectName('url_lineEdit')
        self.search_btn = QtWidgets.QPushButton(dialog)
        self.search_btn.setGeometry(QtCore.QRect(670, 10, 90, 30))
        font = QtGui.QFont()
        font.setPointSize(12)
        self.search_btn.setFont(font)
        self.search_btn.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
        self.search_btn.setObjectName('search_btn')
        self.config_btn = QtWidgets.QPushButton(dialog)
        self.config_btn.setGeometry(QtCore.QRect(780, 10, 60, 30))
        font = QtGui.QFont()
        font.setPointSize(10)
        self.config_btn.setFont(font)
        self.config_btn.setObjectName('config_btn')

        # ----------------------------------------------

        self.url_lineEdit.setText('準備中')
        self.page_2_title.setText('Setting the DNS')
        self.page_2_label1.setText('IP Address')
        self.page_2_ip.setText('準備中')
        self.page_2_label2.setText('URL')
        self.page_2_url.setText('準備中')

        self.webView.setHtml(html_source)

        # ----------------------------------------------

        QtCore.QMetaObject.connectSlotsByName(dialog)


def mybrowser(html_source):
    app = QtWidgets.QApplication(sys.argv)
    dialog = QtWidgets.QDialog()
    ui = Ui_dialog()
    ui.setupUi(dialog, html_source)
    dialog.show()
    sys.exit(app.exec_())
```