EST. 2026 — บันทึกงานเซิร์ฟเวอร์จริง SYSADMIN · SECURITY · AI · DIRECTADMIN เล่มที่ 01
THE SYSADMIN ALMANAC
หน้าหลัก / Hardware / ให้เว็บอ่านเครื่องอ่านบัตรที่เคาน์เตอร์ ด้วย agent บน localhost
Hardware

ให้เว็บอ่านเครื่องอ่านบัตรที่เคาน์เตอร์ ด้วย agent บน localhost

โดย Ton-Or ·16 ต.ค. 2026·อ่าน 11 นาที
ให้เว็บอ่านเครื่องอ่านบัตรที่เคาน์เตอร์ ด้วย agent บน localhost

โจทย์ฟังดูเล็กมาก คือกดปุ่มบนหน้าเว็บแล้วให้ข้อมูลจากบัตรเด้งขึ้นมาในฟอร์ม แต่ระหว่างหน้าเว็บกับเครื่องอ่านบัตรที่วางอยู่ห่างกันครึ่งเมตร มีกำแพงของเบราว์เซอร์กั้นอยู่หลายชั้น และชั้นสุดท้ายคือแอนตี้ไวรัสที่ไม่เชื่อว่าโปรแกรมเราไม่ใช่ backdoor

พอดีมีงานที่หน้าเคาน์เตอร์ต้องกรอกข้อมูลคนจากบัตรประชาชนลงระบบวันละหลายสิบราย เจ้าหน้าที่พิมพ์เองทุกช่อง ผิดบ้างถูกบ้าง แล้วที่โต๊ะก็มีเครื่องอ่านบัตรวางอยู่แล้วแต่ไม่ได้ใช้ เพราะระบบเดิมเป็นโปรแกรมติดตั้งบนเครื่อง ส่วนระบบใหม่เป็นเว็บ ผมเลยรับมาทำสะพานเชื่อมสองฝั่งนี้ ตอนประเมินคิดว่าสองวันจบ ของจริงคือสองสัปดาห์ เพราะโค้ดที่อ่านบัตรได้เขียนเสร็จตั้งแต่วันแรก ส่วนที่เหลือทั้งหมดคือการทำให้เบราว์เซอร์ยอมคุยด้วย และทำให้แอนตี้ไวรัสยอมให้มันรัน

ข้อมูลบัตรและชื่อในบทความเป็นข้อมูลสมมติทั้งหมด เลข IP และพอร์ตเป็นค่าตัวอย่าง ส่วนโค้ดและอาการที่เล่าเป็นของจริง

ทำไมเบราว์เซอร์อ่านเครื่องอ่านบัตรเองไม่ได้

คำถามแรกที่ทุกคนถามคือทำไมไม่ให้หน้าเว็บคุยกับเครื่องอ่านตรง ๆ คำตอบคือเบราว์เซอร์ไม่มีทางเข้าถึงชั้น PC/SC ของระบบปฏิบัติการได้เลย มันเป็นกำแพงที่ตั้งใจกั้นไว้ตั้งแต่ต้น ถ้าหน้าเว็บไหนก็ตามอ่านสมาร์ตการ์ดในเครื่องเราได้ โลกคงพังไปนานแล้ว

ตัวเลือกที่มีจริงเหลือสามทาง หนึ่งคือส่วนขยายของเบราว์เซอร์ซึ่งต้องทำแยกทุกค่ายและตายทุกครั้งที่เบราว์เซอร์เปลี่ยนสถาปัตยกรรมส่วนขยาย สองคือโปรแกรมเต็มรูปแบบบนเครื่องซึ่งก็กลับไปเป็นปัญหาเดิมที่หนีมา สามคือ agent ตัวเล็กที่รันอยู่เบื้องหลัง เปิดพอร์ต HTTP บน localhost แล้วให้หน้าเว็บเรียกผ่าน fetch ผมเลือกทางที่สาม เพราะเป็นทางเดียวที่หน้าเว็บเดิมแทบไม่ต้องแก้อะไร

เส้นทางข้อมูลจากบัตรถึงฟอร์มบัตรในเครื่องอ่าน→PC/SCชั้นของ OS→agent127.0.0.1 พอร์ตท้องถิ่น→fetchจากหน้าเว็บ https→ฟอร์มเติมให้อัตโนมัติ

https คุยกับ http บน 127.0.0.1 ได้ แต่ต้องเข้าใจเงื่อนไข

กำแพงแรกที่เจอคือ mixed content หน้าเว็บเป็น https แต่ agent เป็น http ธรรมดา ปกติเบราว์เซอร์จะบล็อกทันที แต่มีข้อยกเว้นสำคัญคือปลายทางที่เป็นวงลูปแบ็กถือเป็นต้นทางที่เชื่อถือได้ เบราว์เซอร์สมัยใหม่เลยยอมให้ยิงจาก https ไปหา http://127.0.0.1 ได้

จุดที่พลาดกันบ่อยคือไปเขียน http://localhost แทน ซึ่งไม่เหมือนกัน เพราะ localhost ต้องผ่านการแปลงชื่อก่อน ในเครื่องที่มีการตั้งค่า DNS แปลก ๆ หรือ proxy ขององค์กรคั่นอยู่ มันอาจไม่ได้ชี้กลับมาที่เครื่องตัวเอง ผลคือทำงานได้บนเครื่องนักพัฒนา แล้วพังที่หน้างานโดยไม่รู้สาเหตุ ผมเลยใช้เลข 127.0.0.1 ตรง ๆ เสมอ

ใช้เลข 127.0.0.1 อย่าใช้ชื่อ localhost

ทั้งสองอย่างดูเหมือนกันบนเครื่องเรา แต่ชื่อ localhost ต้องผ่านการแปลงชื่อก่อน ซึ่งโดนนโยบาย DNS หรือ proxy ขององค์กรแทรกได้ และในเครื่องที่เปิด IPv6 ชื่อนี้อาจแปลงไปเป็น ::1 ก่อน ถ้า agent เราฟังอยู่เฉพาะ IPv4 ก็ต่อไม่ติดทันที ใช้เลขตรง ๆ ตัดปัญหาทั้งสองข้อในคราวเดียว

ฝั่งหน้าเว็บเรียกง่าย ๆ แค่นี้ แต่ต้องดักกรณีต่อไม่ติดให้ดี เพราะเจ้าหน้าที่ต้องได้ข้อความที่บอกว่าให้ไปเปิดโปรแกรมที่มุมขวาล่าง ไม่ใช่ข้อความ error ดิบ ๆ

async function readCard() {
  const ports = [8189, 8289, 8389];          // ต้องตรงกับลำดับ fallback ฝั่ง agent
  for (const p of ports) {
    try {
      const r = await fetch(`http://127.0.0.1:${p}/read`, {
        method: "GET", mode: "cors", cache: "no-store",
        signal: AbortSignal.timeout(6000),
      });
      if (r.ok) return await r.json();
    } catch (e) { /* ลองพอร์ตถัดไป */ }
  }
  throw new Error("ไม่พบโปรแกรมอ่านบัตร กรุณาเปิดโปรแกรมที่มุมขวาล่างของจอ");
}

CORS กับ preflight ที่ agent ต้องตอบให้ถูก

กำแพงที่สองคือ CORS หน้าเว็บอยู่คนละต้นทางกับ agent แน่นอนอยู่แล้ว agent เลยต้องตอบหัวข้ออนุญาตให้ถูก และที่สำคัญกว่าคือต้องตอบ OPTIONS ให้เป็น ไม่ใช่ตอบ 404 เหมือน path ที่ไม่รู้จัก

กฎที่ผมยึดคือไม่ตอบ Access-Control-Allow-Origin: * เด็ดขาด แม้จะสะดวกกว่ามาก เพราะนั่นแปลว่าเว็บไหนก็ตามที่เจ้าหน้าที่เผลอเปิดในเบราว์เซอร์เดียวกัน สั่งอ่านบัตรที่เสียบอยู่ได้ทันที ผมทำเป็นรายการต้นทางที่อนุญาตแล้วสะท้อนกลับเฉพาะตัวที่ตรง

หน้าเว็บagentเครื่องอ่านบัตรOPTIONS /read พร้อม Origin204 พร้อมหัวข้อ CORSต้นทางไม่ตรง ไม่ใส่หัวข้อให้Max-Age 600 ข้ามข้อ 1-2 สิบนาทีGET /readSCardConnect แบบ SHARED + APDUข้อมูลฟิลด์ (TIS-620)JSON เป็น UTF-8
เบราว์เซอร์ส่ง OPTIONS นำก่อน agent ตอบ 204 พร้อมหัวข้อ CORS เฉพาะต้นทางในรายการ ตั้ง Max-Age ไว้ก็ข้ามขั้นนี้ได้สิบนาที ต่อจากนั้นเป็น GET /read ที่อ่านบัตรแบบ SHARED
static const char* ALLOWED[] = {
    "https://app.example.co.th",
    "https://staging.example.co.th",
    nullptr
};

static bool origin_allowed(const std::string& origin) {
    for (int i = 0; ALLOWED[i]; ++i)
        if (origin == ALLOWED[i]) return true;
    return false;
}

static std::string cors_headers(const std::string& origin) {
    if (!origin_allowed(origin)) return "";          // ไม่ตรง ไม่ใส่หัวข้อให้เลย
    return "Access-Control-Allow-Origin: " + origin + "\r\n"
           "Vary: Origin\r\n"
           "Access-Control-Allow-Methods: GET, OPTIONS\r\n"
           "Access-Control-Allow-Headers: Content-Type\r\n"
           "Access-Control-Max-Age: 600\r\n";
}
// OPTIONS ต้องตอบ 204 พร้อมหัวข้อ CORS ห้ามตกไปเป็น 404
if (method == "OPTIONS") {
    std::string h = cors_headers(origin);
    std::string res = "HTTP/1.1 204 No Content\r\n" + h +
                      "Content-Length: 0\r\nConnection: close\r\n\r\n";
    send_all(sock, res);
    return;
}
Max-Age ช่วยเรื่องความเร็วมากกว่าที่คิด

ถ้าไม่ตั้ง Max-Age เบราว์เซอร์จะยิง OPTIONS นำทุกครั้งก่อนอ่านบัตรทุกใบ ที่เคาน์เตอร์ซึ่งอ่านบัตรติดกันหลายสิบใบต่อชั่วโมง นั่นคือการเชื่อมต่อเพิ่มขึ้นเท่าตัวโดยไม่ได้อะไรเลย ตั้งไว้สักสิบนาทีกำลังดี นานกว่านั้นจะแก้รายการต้นทางที่อนุญาตแล้วเห็นผลช้า

พอร์ตชนกับ agent เจ้าอื่น และกับดัก SO_REUSEADDR บนวินโดวส์

เครื่องที่เคาน์เตอร์ไม่ได้มีแค่โปรแกรมของเรา หลายที่มีโปรแกรมอ่านบัตรของระบบอื่นติดตั้งอยู่ก่อนแล้ว ถ้าไปชนพอร์ตกันคือจบเลย ผมเลยทำ fallback สามพอร์ต ลองไล่ลงไปจนกว่าจะจองได้ แล้วฝั่งหน้าเว็บก็ไล่ลองเรียงเดียวกัน

จุดที่ต้องระวังมากคือบนวินโดวส์ ตัวเลือก SO_REUSEADDR มีความหมายไม่เหมือนบนยูนิกซ์ ที่นั่นมันแปลว่าอนุญาตให้โปรเซสอื่นมาผูกทับพอร์ตเดียวกันได้ ผลคือโปรแกรมเราอาจผูกสำเร็จทั้งที่มีคนอื่นฟังอยู่แล้ว แล้วคำขอจะถูกแบ่งไปคนละทางแบบสุ่ม อาการที่เห็นคือบางครั้งอ่านบัตรได้ บางครั้งไม่ได้ สลับไปมาโดยหาสาเหตุไม่เจอ ตัวที่ต้องใช้คือ SO_EXCLUSIVEADDRUSE ซึ่งบังคับให้การผูกล้มเหลวไปเลยถ้าพอร์ตมีคนใช้อยู่

static SOCKET bind_local(int port) {
    SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (s == INVALID_SOCKET) return INVALID_SOCKET;

    BOOL excl = TRUE;   // บนวินโดวส์ต้องตัวนี้ ไม่ใช่ SO_REUSEADDR
    setsockopt(s, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, (char*)&excl, sizeof(excl));

    sockaddr_in a{};
    a.sin_family = AF_INET;
    a.sin_port   = htons((u_short)port);
    inet_pton(AF_INET, "127.0.0.1", &a.sin_addr);   // ฟังเฉพาะวงลูปแบ็ก

    if (bind(s, (sockaddr*)&a, sizeof(a)) == SOCKET_ERROR ||
        listen(s, 8) == SOCKET_ERROR) {
        closesocket(s);
        return INVALID_SOCKET;
    }
    return s;
}

SOCKET listen_with_fallback(int* chosen) {
    const int ports[] = {8189, 8289, 8389};
    for (int p : ports) {
        SOCKET s = bind_local(p);
        if (s != INVALID_SOCKET) { *chosen = p; return s; }
    }
    return INVALID_SOCKET;
}

อีกข้อที่ต้องทำคือผูกกับ 127.0.0.1 เท่านั้น ห้ามผูกกับทุกอินเทอร์เฟซ ไม่งั้นเครื่องอื่นในวงเดียวกันจะสั่งอ่านบัตรที่เสียบอยู่ที่เคาน์เตอร์เราได้ ข้อนี้สำคัญกว่า CORS ด้วยซ้ำ เพราะ CORS เป็นกฎที่เบราว์เซอร์บังคับให้ ส่วนโปรแกรมที่เขียนเองไม่สนใจ CORS เลย

ผูกกับทุกอินเทอร์เฟซ

ทั้งวงแลนสั่งอ่านบัตรได้

สะดวกตอนทดสอบ แต่แปลว่าใครก็ตามที่อยู่ในวงเดียวกันยิงคำขอเข้ามาได้ตรง ๆ โดยไม่ต้องผ่านเบราว์เซอร์ กฎ CORS ที่เราตั้งไว้ไม่ช่วยอะไรเลยในกรณีนี้

ผูกเฉพาะ 127.0.0.1

เฉพาะโปรแกรมในเครื่องเดียวกัน

ตัดพื้นที่โจมตีจากทั้งวงเหลือแค่เครื่องเดียว แล้วค่อยใช้ CORS เป็นชั้นถัดไปคุมว่าหน้าเว็บไหนเรียกได้ สองชั้นนี้ทำงานคนละหน้าที่ ต้องมีทั้งคู่

คุยกับการ์ดด้วย PC/SC แบบ SHARED ไม่ยึดเครื่องอ่านไว้คนเดียว

ถึงส่วนที่เป็นงานจริงของ agent คือคุยกับบัตร ชั้น PC/SC ของวินโดวส์เปิดให้ต่อได้สองโหมด ถ้าเลือก EXCLUSIVE โปรแกรมเราจะยึดเครื่องอ่านไว้คนเดียว โปรแกรมอื่นที่เคยใช้อยู่ก่อนจะเปิดไม่ได้ทันที ซึ่งเป็นวิธีที่เร็วที่สุดที่จะทำให้ทีมไอทีของหน้างานโกรธเรา ผมใช้ SHARED เสมอ แล้วเปิดต่อเฉพาะตอนจะอ่านจริง อ่านเสร็จตัดทันที

อีกเรื่องคือการรอบัตร อย่าวนถามสถานะถี่ ๆ เพราะเปลืองซีพียูโดยไม่จำเป็นบนเครื่องเคาน์เตอร์ที่มักจะเก่า ใช้ SCardGetStatusChange ที่บล็อกรอจนกว่าสถานะจะเปลี่ยนจริงดีกว่ามาก

SCARDCONTEXT ctx;
SCardEstablishContext(SCARD_SCOPE_USER, nullptr, nullptr, &ctx);

SCARD_READERSTATE st{};
st.szReader      = reader_name;              // ชื่อเครื่องอ่านจาก SCardListReaders
st.dwCurrentState = SCARD_STATE_UNAWARE;

// บล็อกรอจนสถานะเปลี่ยน ไม่ใช่วน poll ทุก 200 ms
LONG rc = SCardGetStatusChange(ctx, 5000, &st, 1);
if (rc == SCARD_S_SUCCESS && (st.dwEventState & SCARD_STATE_PRESENT)) {
    SCARDHANDLE h; DWORD proto = 0;
    // SHARED เท่านั้น อย่ายึดเครื่องอ่านไว้คนเดียว
    SCardConnect(ctx, reader_name, SCARD_SHARE_SHARED,
                 SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1, &h, &proto);
    read_fields(h, proto);
    SCardDisconnect(h, SCARD_LEAVE_CARD);     // ปล่อยคืน ไม่รีเซ็ตบัตร
}

การคุยกับบัตรใช้คำสั่ง APDU ขั้นตอนคือเลือกแอปพลิเคชันบนบัตรก่อน แล้วค่อยสั่งอ่านทีละช่วง ถ้าเป็นโปรโตคอลแบบ T=0 ต้องตามด้วยคำสั่งขอผลลัพธ์อีกหนึ่งคำสั่งเสมอ ตรงนี้เป็นจุดที่พลาดกันบ่อยแล้วได้ข้อมูลว่างเปล่าโดยไม่มี error

// เลือกแอปพลิเคชันบนบัตรก่อน จากนั้นอ่านทีละช่วงตามออฟเซ็ต
static const BYTE SELECT[] = {0x00,0xA4,0x04,0x00,0x08,
                              0xA0,0x00,0x00,0x00,0x54,0x48,0x00,0x01};

// คำขออ่าน แล้วตามด้วยคำสั่งขอผลลัพธ์ (จำเป็นสำหรับ T=0)
static std::vector<BYTE> read_field(SCARDHANDLE h, DWORD proto,
                                    BYTE off_hi, BYTE off_lo, BYTE len) {
    BYTE cmd[]  = {0x80,0xB0, off_hi, off_lo, 0x02, 0x00, len};
    BYTE getr[] = {0x00,0xC0,0x00,0x00, len};
    BYTE buf[260]; DWORD n = sizeof(buf);
    const SCARD_IO_REQUEST* pci =
        (proto == SCARD_PROTOCOL_T1) ? SCARD_PCI_T1 : SCARD_PCI_T0;

    SCardTransmit(h, pci, cmd, sizeof(cmd), nullptr, buf, &n);
    n = sizeof(buf);
    SCardTransmit(h, pci, getr, sizeof(getr), nullptr, buf, &n);
    if (n < 2) return {};
    return std::vector<BYTE>(buf, buf + n - 2);     // ตัด status word สองไบต์ท้าย
}
อ่านเท่าที่จำเป็น และอย่าเก็บลงเครื่อง

ข้อมูลจากบัตรประชาชนเป็นข้อมูลส่วนบุคคลเต็มขั้น กติกาที่ผมบังคับใส่ในตัว agent คืออ่านเฉพาะฟิลด์ที่ฟอร์มปลายทางต้องใช้จริง ส่งกลับทางวงลูปแบ็กแล้วทิ้งจากหน่วยความจำทันที ไม่เขียนลงไฟล์ ไม่เขียนลง log แม้แต่ตอนดีบั๊ก ส่วน log ที่เก็บได้คือเวลาที่อ่าน กับผลว่าสำเร็จหรือไม่ เท่านั้น

ภาษาไทยจากบัตรเป็น TIS-620 ต้องถอดเอง กับเรื่องฟอนต์และ DPI

ข้อมูลตัวอักษรที่ได้จากบัตรไม่ใช่ UTF-8 มันเป็น TIS-620 ทางลัดที่หลายคนใช้คือเรียกฟังก์ชันแปลงของระบบโดยอ้างอิงโค้ดเพจของเครื่อง ซึ่งใช้ได้ตราบเท่าที่เครื่องตั้งค่าภูมิภาคเป็นไทย พอไปเจอเครื่องที่ตั้งเป็นอังกฤษหรือเครื่องที่เปิดโหมด UTF-8 ทั้งระบบ ข้อความจะกลายเป็นขยะทันที ผมเลยถอดเองด้วยตารางตายตัว ไม่พึ่งค่าของระบบเลย ตารางนี้ตรงไปตรงมามาก เพราะช่วง 0xA1 ถึง 0xFB แมปตรงกับช่วงอักษรไทยในยูนิโค้ดแบบบวกออฟเซ็ตคงที่

// TIS-620 -> UTF-8 ไม่พึ่งโค้ดเพจของระบบ
static std::string tis620_to_utf8(const std::vector<BYTE>& in) {
    std::string out;
    for (BYTE b : in) {
        if (b < 0x80) { out.push_back((char)b); continue; }
        if (b >= 0xA1 && b <= 0xFB) {
            unsigned cp = 0x0E00 + (b - 0xA0);      // ช่วงอักษรไทยในยูนิโค้ด
            out.push_back((char)(0xE0 | (cp >> 12)));
            out.push_back((char)(0x80 | ((cp >> 6) & 0x3F)));
            out.push_back((char)(0x80 | (cp & 0x3F)));
        }
        // ไบต์นอกช่วงคือช่องว่างหรือของที่ไม่ได้ใช้ ข้ามไป
    }
    while (!out.empty() && out.back() == ' ') out.pop_back();   // ตัดช่องว่างท้าย
    return out;
}

ผลลัพธ์ที่ส่งกลับหน้าเว็บเป็น JSON ธรรมดา ตัวอย่างข้างล่างเป็นข้อมูลสมมติทั้งหมด

{
  "ok": true,
  "reader": "Generic Smart Card Reader 0",
  "data": {
    "cid": "0-0000-00000-00-0",
    "th_name": "นาย ตัวอย่าง ทดสอบระบบ",
    "en_name": "Mr. Tuayang Thodsobrabob",
    "birth": "2530-01-01",
    "issue": "2565-01-01",
    "expire": "2573-01-01"
  }
}

เรื่องหน้าตาโปรแกรมมีสองข้อที่ต้องทำตั้งแต่แรก ข้อแรกคือฝังฟอนต์ไทยไว้ในตัวไฟล์เลย อย่าหวังพึ่งฟอนต์ในเครื่อง เพราะเครื่องเคาน์เตอร์ที่ถูกลงวินโดวส์ใหม่แบบตัดของออกเยอะ ๆ มีโอกาสไม่มีฟอนต์ที่เราคิดว่าต้องมี ข้อสองคือประกาศให้โปรแกรมรู้จัก DPI ตั้งแต่ต้น ไม่งั้นบนจอที่ตั้งสเกลไว้เกินร้อยเปอร์เซ็นต์ ตัวหนังสือจะเบลอจนอ่านไม่ออก

<!-- ใส่ใน manifest ของ exe ไม่งั้นบนจอสเกล 150% ตัวหนังสือเบลอทั้งหน้าต่าง -->
<application xmlns="urn:schemas-microsoft-com:asm.v3">
  <windowsSettings>
    <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
    <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2</dpiAwareness>
  </windowsSettings>
</application>

โดนแอนตี้ไวรัสเหมาว่าเป็น backdoor

กำแพงสุดท้ายที่ไม่ได้เตรียมใจไว้เลย ตอนเอาไฟล์ไปลงเครื่องจริง แอนตี้ไวรัสลบทิ้งทันทีตั้งแต่ยังไม่ทันเปิด พอไปดูเหตุผลก็เข้าใจได้ทันทีว่าทำไม เพราะสิ่งที่โปรแกรมเราเป็น มันตรงกับลักษณะของ backdoor แทบทุกข้อ

420 KBขนาดไฟล์ที่เล็กผิดปกติ
3 พอร์ตเปิดฟังไล่จนกว่าจะได้
ไม่มีประวัติความน่าเชื่อถือ
2 สัปดาห์เวลาจริงที่ใช้ทั้งงาน

สิ่งที่แก้ปัญหาได้จริงมีสามข้อ ข้อแรกคือเซ็นไฟล์ด้วยใบรับรองสำหรับเซ็นโค้ด อันนี้เป็นตัวตัดสินและเป็นค่าใช้จ่ายที่ต้องยอมจ่าย ข้อสองคือใส่ข้อมูลเวอร์ชันกับชื่อผู้ผลิตในไฟล์ให้ครบ ไฟล์ที่ไม่มีข้อมูลพวกนี้เลยดูน่าสงสัยกว่ามาก ข้อสามคือคอมไพล์แบบผูกไลบรารีเข้าไปในไฟล์เดียว ไม่ต้องพึ่ง redistributable ที่อาจไม่มีในเครื่องปลายทาง

# คอมไพล์ให้เป็นไฟล์เดียวจบ ไม่ต้องพึ่ง runtime ภายนอก และบังคับซอร์สเป็น UTF-8
cl /std:c++17 /MT /utf-8 /O2 /DNDEBUG ^
   agent.cpp /link winscard.lib ws2_32.lib /OUT:card-agent.exe

# เซ็นไฟล์ด้วยใบรับรองสำหรับเซ็นโค้ด ตัวนี้คือตัวตัดสินจริง
signtool sign /fd SHA256 /tr http://timestamp.example.org /td SHA256 card-agent.exe
อย่าแก้ด้วยการสั่งให้ยกเว้นทั้งโฟลเดอร์

ทางลัดที่ทีมหน้างานชอบเสนอคือใส่ข้อยกเว้นให้โฟลเดอร์ของโปรแกรมทั้งก้อน ซึ่งแก้อาการได้จริงแต่เปิดรูให้ทุกอย่างที่ถูกวางลงโฟลเดอร์นั้นในอนาคตด้วย และต้องไปทำซ้ำทุกเครื่อง ทางที่ถูกคือเซ็นไฟล์ให้เรียบร้อยแล้วเรื่องจบเองทุกเครื่องพร้อมกัน

สรุปโครงที่ผมใช้ กับลำดับที่ควรทำ

  1. ตัดสินใจสถาปัตยกรรมก่อน agent บนวงลูปแบ็กคือทางที่หน้าเว็บเดิมแก้น้อยที่สุด และไม่ผูกกับเบราว์เซอร์ค่ายไหน
  2. ฝั่งเว็บเรียกด้วยเลข 127.0.0.1 เสมอ พร้อม fallback หลายพอร์ตและข้อความผิดพลาดที่คนหน้าเคาน์เตอร์อ่านรู้เรื่อง
  3. ฝั่ง agent ผูกเฉพาะวงลูปแบ็ก ใช้ SO_EXCLUSIVEADDRUSE แล้วไล่พอร์ตจนกว่าจะจองได้จริง
  4. ตอบ OPTIONS ให้ถูกเป็น 204 พร้อมหัวข้อ CORS สะท้อนเฉพาะต้นทางในรายการที่อนุญาต ห้ามใช้เครื่องหมายดอกจัน
  5. ต่อ PC/SC แบบ SHARED รอด้วย SCardGetStatusChange แล้วตัดการเชื่อมต่อทันทีที่อ่านเสร็จ
  6. ถอด TIS-620 เองด้วยตารางตายตัว ไม่พึ่งโค้ดเพจของเครื่อง ฝังฟอนต์ไทยและประกาศ DPI ในไฟล์
  7. เซ็นไฟล์ด้วยใบรับรองสำหรับเซ็นโค้ดตั้งแต่รอบแรกที่จะเอาไปลงจริง อย่ารอให้โดนลบก่อนแล้วค่อยทำ

ส่วนตัวผมคิดว่างานประเภทสะพานเชื่อมเบราว์เซอร์กับฮาร์ดแวร์แบบนี้ เวลาที่ใช้จริงไม่ได้อยู่ที่โปรโตคอลของอุปกรณ์เลย โค้ดที่อ่านบัตรได้จริงมีไม่ถึงสองร้อยบรรทัด ที่เหลือคือการทำให้มันอยู่รอดในเครื่องของคนอื่น ที่มีโปรแกรมอื่นอยู่ก่อน มีนโยบายความปลอดภัยขององค์กรคุมอยู่ และมีคนใช้ที่ไม่ควรต้องมาเข้าใจว่า CORS คืออะไร ใครจะเอาโครงนี้ไปทำกับอุปกรณ์อื่นที่เบราว์เซอร์แตะไม่ได้ เช่นตาชั่งหรือเครื่องพิมพ์เฉพาะทาง โครงเดียวกันใช้ได้หมด เปลี่ยนแค่ชั้นที่คุยกับอุปกรณ์

ที่ยังค้างในแผนของผมคือทำตัวอัปเดตอัตโนมัติให้ agent เพราะตอนนี้เวลาแก้อะไรต้องไล่เดินไปทีละเคาน์เตอร์ กับย้ายไปใช้ WebSocket แทนการเรียกแบบถามครั้งเดียว เพื่อให้หน้าเว็บรู้ทันทีตอนมีคนเสียบบัตร โดยที่เจ้าหน้าที่ไม่ต้องกดปุ่มเลย ตรงไหนผมอธิบายตกหล่นหรือพิมพ์ผิด เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ

อยากให้เราทำให้ไหม?

ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า

คุยเรื่องงานกับเรา

เคยเจองานแบบนี้ไหม? ติดตรงไหน?

มาแลกวิธีกันต่อในบอร์ด LinuxThai

→ คุยต่อในบอร์ด

ความคิดเห็น 0

ยังไม่มีความคิดเห็น — เป็นคนแรกเลย