Mã hóa / Giải mã URL trong C ++ [đóng]


85

Có ai biết về bất kỳ mã C ++ tốt nào làm điều này không?


3
Làm thế nào về việc chấp nhận một câu trả lời?
gsamaras

Câu trả lời:


81

Tôi đã đối mặt với một nửa mã hóa của vấn đề này vào ngày hôm trước. Không hài lòng với các tùy chọn có sẵn và sau khi xem mã mẫu C này , tôi đã quyết định triển khai chức năng mã hóa url C ++ của riêng mình:

#include <cctype>
#include <iomanip>
#include <sstream>
#include <string>

using namespace std;

string url_encode(const string &value) {
    ostringstream escaped;
    escaped.fill('0');
    escaped << hex;

    for (string::const_iterator i = value.begin(), n = value.end(); i != n; ++i) {
        string::value_type c = (*i);

        // Keep alphanumeric and other accepted characters intact
        if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
            escaped << c;
            continue;
        }

        // Any other characters are percent-encoded
        escaped << uppercase;
        escaped << '%' << setw(2) << int((unsigned char) c);
        escaped << nouppercase;
    }

    return escaped.str();
}

Việc thực hiện các chức năng giải mã được để lại như một bài tập cho người đọc. : P


1
Tôi tin rằng việc thay thế '' bằng "% 20" sẽ chung chung hơn (thường đúng hơn). Tôi đã cập nhật mã cho phù hợp; vui lòng quay lại nếu bạn không đồng ý.
Josh Kelley

1
Không, tôi đồng ý. Cũng đã có cơ hội để loại bỏ setw(0)lệnh gọi vô nghĩa đó (tại thời điểm tôi nghĩ chiều rộng tối thiểu sẽ vẫn được đặt cho đến khi tôi thay đổi lại, nhưng trên thực tế nó được đặt lại sau lần nhập tiếp theo).
xperroni

1
Tôi phải thêm std :: chữ hoa vào dòng "Escape << '%' << std :: uppercase << std :: setw (2) << int ((unsigned char) c);" Trong trường hợp người khác đang tự hỏi tại sao lợi nhuận này ví dụ% 3a thay vì% 3A
gumlym

2
Có vẻ sai vì chuỗi UTF-8 không được hỗ trợ ( w3schools.com/tags/ref_urlencode.asp ). Có vẻ như nó chỉ hoạt động với Windows-1252
Skywalker 13

1
Vấn đề chỉ là isalnum(c), nó phải được đổi thànhisalnum((unsigned char) c)
Skywalker. 13

74

Trả lời câu hỏi của riêng tôi ...

libcurl có curl_easy_escape để mã hóa.

Để giải mã, curl_easy_unescape


4
Bạn nên chấp nhận câu trả lời này để nó được hiển thị ở trên cùng (và mọi người có thể tìm thấy nó dễ dàng hơn).
Mouagip

bạn cần phải sử dụng curl cho việc này đến công việc và phải giải phóng bộ nhớ
xinthose

Câu hỏi liên quan: tại sao unescape của curl không xử lý việc thay đổi '+' thành dấu cách? Đó không phải là thủ tục tiêu chuẩn khi giải mã URL?
Stéphane

12
string urlDecode(string &SRC) {
    string ret;
    char ch;
    int i, ii;
    for (i=0; i<SRC.length(); i++) {
        if (int(SRC[i])==37) {
            sscanf(SRC.substr(i+1,2).c_str(), "%x", &ii);
            ch=static_cast<char>(ii);
            ret+=ch;
            i=i+2;
        } else {
            ret+=SRC[i];
        }
    }
    return (ret);
}

không phải là tốt nhất, nhưng hoạt động tốt ;-)


5
Tất nhiên bạn nên sử dụng '%'thay vì 37.
John Zwinck

4
Điều này không chuyển đổi '+' để không gian
xryl669

11

cpp-netlib có các chức năng

namespace boost {
  namespace network {
    namespace uri {    
      inline std::string decoded(const std::string &input);
      inline std::string encoded(const std::string &input);
    }
  }
}

chúng cho phép mã hóa và giải mã chuỗi URL rất dễ dàng.


2
omg cảm ơn bạn. tài liệu về cpp-netlib rất thưa thớt. Bạn có bất kỳ liên kết đến các trang gian lận tốt không?
user249806

8

Thông thường thêm '%' vào giá trị int của một ký tự sẽ không hoạt động khi mã hóa, giá trị được cho là tương đương hex. ví dụ: '/' là '% 2F' không phải '% 47'.

Tôi nghĩ đây là giải pháp tốt nhất và ngắn gọn cho cả mã hóa và giải mã url (Không có nhiều phụ thuộc tiêu đề).

string urlEncode(string str){
    string new_str = "";
    char c;
    int ic;
    const char* chars = str.c_str();
    char bufHex[10];
    int len = strlen(chars);

    for(int i=0;i<len;i++){
        c = chars[i];
        ic = c;
        // uncomment this if you want to encode spaces with +
        /*if (c==' ') new_str += '+';   
        else */if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') new_str += c;
        else {
            sprintf(bufHex,"%X",c);
            if(ic < 16) 
                new_str += "%0"; 
            else
                new_str += "%";
            new_str += bufHex;
        }
    }
    return new_str;
 }

string urlDecode(string str){
    string ret;
    char ch;
    int i, ii, len = str.length();

    for (i=0; i < len; i++){
        if(str[i] != '%'){
            if(str[i] == '+')
                ret += ' ';
            else
                ret += str[i];
        }else{
            sscanf(str.substr(i + 1, 2).c_str(), "%x", &ii);
            ch = static_cast<char>(ii);
            ret += ch;
            i = i + 2;
        }
    }
    return ret;
}

if(ic < 16) new_str += "%0"; Cái này phục vụ cho cái gì ?? @tormuto @reliasn
KriyenKP

1
@Kriyen nó được sử dụng để đệm HEX được mã hóa với số 0 ở đầu trong trường hợp nó dẫn đến một chữ cái duy nhất; vì 0 đến 15 trong HEX là 0 đến F.
tormuto

1
Tôi thích cách tiếp cận này nhất. +1 để sử dụng thư viện tiêu chuẩn. Mặc dù có hai vấn đề cần khắc phục. Tôi là người Séc và sử dụng chữ cái "ý". Kết quả là "% 0FFFFFFC3% 0FFFFFFBD". Đầu tiên, sử dụng công tắc 16 là không cần thiết vì utf8 đảm bảo bắt đầu tất cả các byte theo sau bằng 10 và nó dường như không thành công đa byte của tôi. Vấn đề thứ hai là FF vì không phải tất cả các máy tính đều có cùng số lượng bit trên mỗi int. Cách khắc phục là bỏ qua công tắc 16 (không cần thiết) và lấy hai ký tự cuối cùng từ bộ đệm. (Tôi đã sử dụng stringstream vì tôi cảm thấy thoải mái hơn với bộ đệm chuỗi). Vẫn cho điểm. Thích khung quá
Volt

@Volt bạn có thể đăng mã cập nhật của mình trong một câu trả lời mới không? Bạn đề cập đến các vấn đề nhưng nó không đủ thông tin để khắc phục rõ ràng.
gregn3

Câu trả lời này có một số vấn đề, vì nó đang sử dụng strlen. Đầu tiên, điều này không có ý nghĩa, bởi vì chúng ta đã biết kích thước của một đối tượng chuỗi, vì vậy thật lãng phí thời gian. Tuy nhiên, tệ hơn nhiều là một chuỗi có thể chứa 0 byte, sẽ bị mất do strlen. Ngoài ra if (i <16) là không hiệu quả, vì điều này có thể được printf tự che bằng cách sử dụng "%%% 02X". Và cuối cùng c phải là byte không dấu, nếu không bạn sẽ nhận được hiệu ứng mà @Volt đang mô tả với '0xFFF ...'.
Devolus

8

[Bật chế độ Necromancer]
Tình cờ gặp câu hỏi này khi đang tìm kiếm giải pháp nhanh, hiện đại, độc lập với nền tảng và thanh lịch. Không giống như bất kỳ điều nào ở trên, cpp-netlib sẽ là người chiến thắng nhưng nó có lỗ hổng bộ nhớ khủng khiếp trong chức năng "giải mã". Vì vậy, tôi đã nghĩ ra giải pháp khí lực / nghiệp lực của boost.

namespace bsq = boost::spirit::qi;
namespace bk = boost::spirit::karma;
bsq::int_parser<unsigned char, 16, 2, 2> hex_byte;
template <typename InputIterator>
struct unescaped_string
    : bsq::grammar<InputIterator, std::string(char const *)> {
  unescaped_string() : unescaped_string::base_type(unesc_str) {
    unesc_char.add("+", ' ');

    unesc_str = *(unesc_char | "%" >> hex_byte | bsq::char_);
  }

  bsq::rule<InputIterator, std::string(char const *)> unesc_str;
  bsq::symbols<char const, char const> unesc_char;
};

template <typename OutputIterator>
struct escaped_string : bk::grammar<OutputIterator, std::string(char const *)> {
  escaped_string() : escaped_string::base_type(esc_str) {

    esc_str = *(bk::char_("a-zA-Z0-9_.~-") | "%" << bk::right_align(2,0)[bk::hex]);
  }
  bk::rule<OutputIterator, std::string(char const *)> esc_str;
};

Cách sử dụng ở trên như sau:

std::string unescape(const std::string &input) {
  std::string retVal;
  retVal.reserve(input.size());
  typedef std::string::const_iterator iterator_type;

  char const *start = "";
  iterator_type beg = input.begin();
  iterator_type end = input.end();
  unescaped_string<iterator_type> p;

  if (!bsq::parse(beg, end, p(start), retVal))
    retVal = input;
  return retVal;
}

std::string escape(const std::string &input) {
  typedef std::back_insert_iterator<std::string> sink_type;
  std::string retVal;
  retVal.reserve(input.size() * 3);
  sink_type sink(retVal);
  char const *start = "";

  escaped_string<sink_type> g;
  if (!bk::generate(sink, g(start), input))
    retVal = input;
  return retVal;
}

[Chế độ Necromancer đang tắt]

EDIT01: sửa lỗi không đệm - đặc biệt cảm ơn Hartmut Kaiser
EDIT02: Trực tiếp trên CoLiRu


“Lỗ hổng bộ nhớ khủng khiếp” là cpp-netlibgì? Bạn có thể cung cấp một lời giải thích ngắn gọn hoặc một liên kết?
Craig M. Brandenburg,

Nó (vấn đề) đã được báo cáo, vì vậy tôi báo cáo didnt và thực sự không nhớ ... một cái gì đó giống như vi phạm truy cập khi cố gắng phân tích chuỗi không hợp lệ thoát, hoặc một cái gì đó
kreuzerkrieg


Cảm ơn đã làm rõ!
Craig M. Brandenburg

6

CGICC bao gồm các phương pháp để mã hóa và giải mã url. form_urlencode và form_urldecode


bạn vừa khơi mào một cuộc trò chuyện tử tế trong văn phòng của chúng tôi với thư viện đó.
JJ

1
Đây thực sự là mã đơn giản nhất và chính xác nhất.
xryl669

6

Lấy cảm hứng từ xperroni, tôi đã viết một bộ giải mã. Cảm ơn bạn cho con trỏ.

#include <iostream>
#include <sstream>
#include <string>

using namespace std;

char from_hex(char ch) {
    return isdigit(ch) ? ch - '0' : tolower(ch) - 'a' + 10;
}

string url_decode(string text) {
    char h;
    ostringstream escaped;
    escaped.fill('0');

    for (auto i = text.begin(), n = text.end(); i != n; ++i) {
        string::value_type c = (*i);

        if (c == '%') {
            if (i[1] && i[2]) {
                h = from_hex(i[1]) << 4 | from_hex(i[2]);
                escaped << h;
                i += 2;
            }
        } else if (c == '+') {
            escaped << ' ';
        } else {
            escaped << c;
        }
    }

    return escaped.str();
}

int main(int argc, char** argv) {
    string msg = "J%C3%B8rn!";
    cout << msg << endl;
    string decodemsg = url_decode(msg);
    cout << decodemsg << endl;

    return 0;
}

chỉnh sửa: Đã xóa cctype không cần thiết và bao gồm iomainip.


1
Khối "if (c == '%')" cần thêm kiểm tra ngoài giới hạn, i [1] và / hoặc i [2] có thể nằm ngoài text.end (). Tôi cũng sẽ đổi tên "Escape" thành "unescaped". "Escape.fill ('0');" có lẽ là không cần thiết.
roalz 23/03/18

Làm ơn, hãy nhìn vào phiên bản của tôi. Nó được tối ưu hóa hơn. pastebin.com/g0zMLpsj
KoD

4

Thêm thông tin tiếp theo vào khuyến nghị của Bill về việc sử dụng libcurl: đề xuất tuyệt vời và sẽ được cập nhật:
sau 3 năm, hàm curl_escape không còn được dùng nữa, vì vậy, để sử dụng trong tương lai, tốt hơn nên sử dụng curl_easy_escape .


4

Tôi đã kết thúc câu hỏi này khi tìm kiếm một api để giải mã url trong ứng dụng win32 c ++. Vì câu hỏi không xác định rõ nền tảng nên giả sử cửa sổ không phải là một điều xấu.

InternetCanonicalizeUrl là API cho các chương trình windows. Thông tin thêm tại đây

        LPTSTR lpOutputBuffer = new TCHAR[1];
        DWORD dwSize = 1;
        BOOL fRes = ::InternetCanonicalizeUrl(strUrl, lpOutputBuffer, &dwSize, ICU_DECODE | ICU_NO_ENCODE);
        DWORD dwError = ::GetLastError();
        if (!fRes && dwError == ERROR_INSUFFICIENT_BUFFER)
        {
            delete lpOutputBuffer;
            lpOutputBuffer = new TCHAR[dwSize];
            fRes = ::InternetCanonicalizeUrl(strUrl, lpOutputBuffer, &dwSize, ICU_DECODE | ICU_NO_ENCODE);
            if (fRes)
            {
                //lpOutputBuffer has decoded url
            }
            else
            {
                //failed to decode
            }
            if (lpOutputBuffer !=NULL)
            {
                delete [] lpOutputBuffer;
                lpOutputBuffer = NULL;
            }
        }
        else
        {
            //some other error OR the input string url is just 1 char and was successfully decoded
        }

InternetCrackUrl ( ở đây ) dường như cũng có cờ để chỉ định xem có giải mã url hay không


3

Tôi không thể tìm thấy giải mã URI / unescape ở đây cũng giải mã chuỗi 2 và 3 byte. Đóng góp phiên bản hiệu suất cao của riêng tôi, chuyển đổi trực tiếp đầu vào c sting thành một chuỗi:

#include <string>

const char HEX2DEC[55] =
{
     0, 1, 2, 3,  4, 5, 6, 7,  8, 9,-1,-1, -1,-1,-1,-1,
    -1,10,11,12, 13,14,15,-1, -1,-1,-1,-1, -1,-1,-1,-1,
    -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
    -1,10,11,12, 13,14,15
};

#define __x2d__(s) HEX2DEC[*(s)-48]
#define __x2d2__(s) __x2d__(s) << 4 | __x2d__(s+1)

std::wstring decodeURI(const char * s) {
    unsigned char b;
    std::wstring ws;
    while (*s) {
        if (*s == '%')
            if ((b = __x2d2__(s + 1)) >= 0x80) {
                if (b >= 0xE0) { // three byte codepoint
                    ws += ((b & 0b00001111) << 12) | ((__x2d2__(s + 4) & 0b00111111) << 6) | (__x2d2__(s + 7) & 0b00111111);
                    s += 9;
                }
                else { // two byte codepoint
                    ws += (__x2d2__(s + 4) & 0b00111111) | (b & 0b00000011) << 6;
                    s += 6;
                }
            }
            else { // one byte codepoints
                ws += b;
                s += 3;
            }
        else { // no %
            ws += *s;
            s++;
        }
    }
    return ws;
}

#define __x2d2__(s) (__x2d__(s) << 4 | __x2d__(s+1))và nó sẽ xây dựng với -WError.
Janek Olszak

Xin lỗi nhưng "hiệu suất cao" trong khi thêm các ký tự đơn vào a wstringlà không thực tế. Ít nhất reservekhông gian đủ, nếu không bạn sẽ phải tái phân bổ lớn mọi lúc
Felix Dombek


1

Phiên bản này là thuần C và có thể tùy chọn chuẩn hóa đường dẫn tài nguyên. Sử dụng nó với C ++ là không bình thường:

#include <string>
#include <iostream>

int main(int argc, char** argv)
{
    const std::string src("/some.url/foo/../bar/%2e/");
    std::cout << "src=\"" << src << "\"" << std::endl;

    // either do it the C++ conformant way:
    char* dst_buf = new char[src.size() + 1];
    urldecode(dst_buf, src.c_str(), 1);
    std::string dst1(dst_buf);
    delete[] dst_buf;
    std::cout << "dst1=\"" << dst1 << "\"" << std::endl;

    // or in-place with the &[0] trick to skip the new/delete
    std::string dst2;
    dst2.resize(src.size() + 1);
    dst2.resize(urldecode(&dst2[0], src.c_str(), 1));
    std::cout << "dst2=\"" << dst2 << "\"" << std::endl;
}

Kết quả đầu ra:

src="/some.url/foo/../bar/%2e/"
dst1="/some.url/bar/"
dst2="/some.url/bar/"

Và chức năng thực tế:

#include <stddef.h>
#include <ctype.h>

/**
 * decode a percent-encoded C string with optional path normalization
 *
 * The buffer pointed to by @dst must be at least strlen(@src) bytes.
 * Decoding stops at the first character from @src that decodes to null.
 * Path normalization will remove redundant slashes and slash+dot sequences,
 * as well as removing path components when slash+dot+dot is found. It will
 * keep the root slash (if one was present) and will stop normalization
 * at the first questionmark found (so query parameters won't be normalized).
 *
 * @param dst       destination buffer
 * @param src       source buffer
 * @param normalize perform path normalization if nonzero
 * @return          number of valid characters in @dst
 * @author          Johan Lindh <johan@linkdata.se>
 * @legalese        BSD licensed (http://opensource.org/licenses/BSD-2-Clause)
 */
ptrdiff_t urldecode(char* dst, const char* src, int normalize)
{
    char* org_dst = dst;
    int slash_dot_dot = 0;
    char ch, a, b;
    do {
        ch = *src++;
        if (ch == '%' && isxdigit(a = src[0]) && isxdigit(b = src[1])) {
            if (a < 'A') a -= '0';
            else if(a < 'a') a -= 'A' - 10;
            else a -= 'a' - 10;
            if (b < 'A') b -= '0';
            else if(b < 'a') b -= 'A' - 10;
            else b -= 'a' - 10;
            ch = 16 * a + b;
            src += 2;
        }
        if (normalize) {
            switch (ch) {
            case '/':
                if (slash_dot_dot < 3) {
                    /* compress consecutive slashes and remove slash-dot */
                    dst -= slash_dot_dot;
                    slash_dot_dot = 1;
                    break;
                }
                /* fall-through */
            case '?':
                /* at start of query, stop normalizing */
                if (ch == '?')
                    normalize = 0;
                /* fall-through */
            case '\0':
                if (slash_dot_dot > 1) {
                    /* remove trailing slash-dot-(dot) */
                    dst -= slash_dot_dot;
                    /* remove parent directory if it was two dots */
                    if (slash_dot_dot == 3)
                        while (dst > org_dst && *--dst != '/')
                            /* empty body */;
                    slash_dot_dot = (ch == '/') ? 1 : 0;
                    /* keep the root slash if any */
                    if (!slash_dot_dot && dst == org_dst && *dst == '/')
                        ++dst;
                }
                break;
            case '.':
                if (slash_dot_dot == 1 || slash_dot_dot == 2) {
                    ++slash_dot_dot;
                    break;
                }
                /* fall-through */
            default:
                slash_dot_dot = 0;
            }
        }
        *dst++ = ch;
    } while(ch);
    return (dst - org_dst) - 1;
}

Cảm ơn. Đây là nó không có công cụ đường dẫn tùy chọn. pastebin.com/RN5g7g9u
Julian

Điều này không tuân theo bất kỳ lệnh gợi ý nào và hoàn toàn sai so với những gì tác giả yêu cầu (ví dụ: '+' không được thay thế bằng dấu cách). Chuẩn hóa đường dẫn không liên quan gì đến giải mã url. Nếu bạn có ý định chuẩn hóa đường dẫn của mình, trước tiên bạn nên chia URL của mình thành các phần (lược đồ, thẩm quyền, đường dẫn, truy vấn, phân đoạn) và sau đó áp dụng bất kỳ thuật toán nào bạn thích chỉ trên phần đường dẫn.
xryl669

1

các bit ngon ngọt

#include <ctype.h> // isdigit, tolower

from_hex(char ch) {
  return isdigit(ch) ? ch - '0' : tolower(ch) - 'a' + 10;
}

char to_hex(char code) {
  static char hex[] = "0123456789abcdef";
  return hex[code & 15];
}

ghi chú điều đó

char d = from_hex(hex[0]) << 4 | from_hex(hex[1]);

như trong

// %7B = '{'

char d = from_hex('7') << 4 | from_hex('B');

1

Bạn có thể sử dụng hàm "g_uri_escape_string ()" được cung cấp glib.h. https://developer.gnome.org/glib/stable/glib-URI-Functions.html

#include <stdio.h>
#include <stdlib.h>
#include <glib.h>
int main() {
    char *uri = "http://www.example.com?hello world";
    char *encoded_uri = NULL;
    //as per wiki (https://en.wikipedia.org/wiki/Percent-encoding)
    char *escape_char_str = "!*'();:@&=+$,/?#[]"; 
    encoded_uri = g_uri_escape_string(uri, escape_char_str, TRUE);
    printf("[%s]\n", encoded_uri);
    free(encoded_uri);

    return 0;
}

biên dịch nó với:

gcc encoding_URI.c `pkg-config --cflags --libs glib-2.0`


0

Tôi biết câu hỏi yêu cầu một phương thức C ++, nhưng đối với những người có thể cần nó, tôi đã nghĩ ra một hàm rất ngắn trong C đơn giản để mã hóa một chuỗi. Nó không tạo một chuỗi mới, thay vào đó nó thay đổi chuỗi hiện có, nghĩa là nó phải có đủ kích thước để chứa chuỗi mới. Rất dễ dàng để theo kịp.

void urlEncode(char *string)
{
    char charToEncode;
    int posToEncode;
    while (((posToEncode=strspn(string,"1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_.~"))!=0) &&(posToEncode<strlen(string)))
    {
        charToEncode=string[posToEncode];
        memmove(string+posToEncode+3,string+posToEncode+1,strlen(string+posToEncode));
        string[posToEncode]='%';
        string[posToEncode+1]="0123456789ABCDEF"[charToEncode>>4];
        string[posToEncode+2]="0123456789ABCDEF"[charToEncode&0xf];
        string+=posToEncode+3;
    }
}

0

bạn có thể chỉ cần sử dụng hàm AtlEscapeUrl () từ atlutil.h, chỉ cần xem qua tài liệu về cách sử dụng nó.


1
này sẽ chỉ làm việc trên cửa sổ
kritzikratzi

Có, tôi đã thử điều này trên windows.
Pratik

-2

Phải làm điều đó trong một dự án mà không có Boost. Vì vậy, cuối cùng đã viết của riêng tôi. Tôi sẽ chỉ đưa nó lên GitHub: https://github.com/corporatehark/LUrlParser

clParseURL URL = clParseURL::ParseURL( "https://name:pwd@github.com:80/path/res" );

if ( URL.IsValid() )
{
    cout << "Scheme    : " << URL.m_Scheme << endl;
    cout << "Host      : " << URL.m_Host << endl;
    cout << "Port      : " << URL.m_Port << endl;
    cout << "Path      : " << URL.m_Path << endl;
    cout << "Query     : " << URL.m_Query << endl;
    cout << "Fragment  : " << URL.m_Fragment << endl;
    cout << "User name : " << URL.m_UserName << endl;
    cout << "Password  : " << URL.m_Password << endl;
}

Liên kết của bạn là đến một thư viện phân tích cú pháp URL. Nó không% -encode một URL. (Hoặc ít nhất, tôi không thể thấy% ở bất kỳ đâu trong nguồn.) Vì vậy, tôi không nghĩ điều này trả lời câu hỏi.
Martin Bonner hỗ trợ Monica
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.