ให้เว็บอ่านเครื่องอ่านบัตรที่เคาน์เตอร์ ด้วย agent บน localhost
โจทย์ฟังดูเล็กมาก คือกดปุ่มบนหน้าเว็บแล้วให้ข้อมูลจากบัตรเด้งขึ้นมาในฟอร์ม แต่ระหว่างหน้าเว็บกับเครื่องอ่านบัตรที่วางอยู่ห่างกันครึ่งเมตร มีกำแพงของเบราว์เซอร์กั้นอยู่หลายชั้น และชั้นสุดท้ายคือแอนตี้ไวรัสที่ไม่เชื่อว่าโปรแกรมเราไม่ใช่ backdoor
พอดีมีงานที่หน้าเคาน์เตอร์ต้องกรอกข้อมูลคนจากบัตรประชาชนลงระบบวันละหลายสิบราย เจ้าหน้าที่พิมพ์เองทุกช่อง ผิดบ้างถูกบ้าง แล้วที่โต๊ะก็มีเครื่องอ่านบัตรวางอยู่แล้วแต่ไม่ได้ใช้ เพราะระบบเดิมเป็นโปรแกรมติดตั้งบนเครื่อง ส่วนระบบใหม่เป็นเว็บ ผมเลยรับมาทำสะพานเชื่อมสองฝั่งนี้ ตอนประเมินคิดว่าสองวันจบ ของจริงคือสองสัปดาห์ เพราะโค้ดที่อ่านบัตรได้เขียนเสร็จตั้งแต่วันแรก ส่วนที่เหลือทั้งหมดคือการทำให้เบราว์เซอร์ยอมคุยด้วย และทำให้แอนตี้ไวรัสยอมให้มันรัน
ข้อมูลบัตรและชื่อในบทความเป็นข้อมูลสมมติทั้งหมด เลข IP และพอร์ตเป็นค่าตัวอย่าง ส่วนโค้ดและอาการที่เล่าเป็นของจริง
ทำไมเบราว์เซอร์อ่านเครื่องอ่านบัตรเองไม่ได้
คำถามแรกที่ทุกคนถามคือทำไมไม่ให้หน้าเว็บคุยกับเครื่องอ่านตรง ๆ คำตอบคือเบราว์เซอร์ไม่มีทางเข้าถึงชั้น PC/SC ของระบบปฏิบัติการได้เลย มันเป็นกำแพงที่ตั้งใจกั้นไว้ตั้งแต่ต้น ถ้าหน้าเว็บไหนก็ตามอ่านสมาร์ตการ์ดในเครื่องเราได้ โลกคงพังไปนานแล้ว
ตัวเลือกที่มีจริงเหลือสามทาง หนึ่งคือส่วนขยายของเบราว์เซอร์ซึ่งต้องทำแยกทุกค่ายและตายทุกครั้งที่เบราว์เซอร์เปลี่ยนสถาปัตยกรรมส่วนขยาย สองคือโปรแกรมเต็มรูปแบบบนเครื่องซึ่งก็กลับไปเป็นปัญหาเดิมที่หนีมา สามคือ agent ตัวเล็กที่รันอยู่เบื้องหลัง เปิดพอร์ต HTTP บน localhost แล้วให้หน้าเว็บเรียกผ่าน fetch ผมเลือกทางที่สาม เพราะเป็นทางเดียวที่หน้าเว็บเดิมแทบไม่ต้องแก้อะไร
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 ตรง ๆ เสมอ
ทั้งสองอย่างดูเหมือนกันบนเครื่องเรา แต่ชื่อ 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: * เด็ดขาด แม้จะสะดวกกว่ามาก เพราะนั่นแปลว่าเว็บไหนก็ตามที่เจ้าหน้าที่เผลอเปิดในเบราว์เซอร์เดียวกัน สั่งอ่านบัตรที่เสียบอยู่ได้ทันที ผมทำเป็นรายการต้นทางที่อนุญาตแล้วสะท้อนกลับเฉพาะตัวที่ตรง
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 เบราว์เซอร์จะยิง 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 ที่เราตั้งไว้ไม่ช่วยอะไรเลยในกรณีนี้
เฉพาะโปรแกรมในเครื่องเดียวกัน
ตัดพื้นที่โจมตีจากทั้งวงเหลือแค่เครื่องเดียว แล้วค่อยใช้ 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 แทบทุกข้อ
สิ่งที่แก้ปัญหาได้จริงมีสามข้อ ข้อแรกคือเซ็นไฟล์ด้วยใบรับรองสำหรับเซ็นโค้ด อันนี้เป็นตัวตัดสินและเป็นค่าใช้จ่ายที่ต้องยอมจ่าย ข้อสองคือใส่ข้อมูลเวอร์ชันกับชื่อผู้ผลิตในไฟล์ให้ครบ ไฟล์ที่ไม่มีข้อมูลพวกนี้เลยดูน่าสงสัยกว่ามาก ข้อสามคือคอมไพล์แบบผูกไลบรารีเข้าไปในไฟล์เดียว ไม่ต้องพึ่ง 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
ทางลัดที่ทีมหน้างานชอบเสนอคือใส่ข้อยกเว้นให้โฟลเดอร์ของโปรแกรมทั้งก้อน ซึ่งแก้อาการได้จริงแต่เปิดรูให้ทุกอย่างที่ถูกวางลงโฟลเดอร์นั้นในอนาคตด้วย และต้องไปทำซ้ำทุกเครื่อง ทางที่ถูกคือเซ็นไฟล์ให้เรียบร้อยแล้วเรื่องจบเองทุกเครื่องพร้อมกัน
สรุปโครงที่ผมใช้ กับลำดับที่ควรทำ
- ตัดสินใจสถาปัตยกรรมก่อน agent บนวงลูปแบ็กคือทางที่หน้าเว็บเดิมแก้น้อยที่สุด และไม่ผูกกับเบราว์เซอร์ค่ายไหน
- ฝั่งเว็บเรียกด้วยเลข 127.0.0.1 เสมอ พร้อม fallback หลายพอร์ตและข้อความผิดพลาดที่คนหน้าเคาน์เตอร์อ่านรู้เรื่อง
- ฝั่ง agent ผูกเฉพาะวงลูปแบ็ก ใช้ SO_EXCLUSIVEADDRUSE แล้วไล่พอร์ตจนกว่าจะจองได้จริง
- ตอบ OPTIONS ให้ถูกเป็น 204 พร้อมหัวข้อ CORS สะท้อนเฉพาะต้นทางในรายการที่อนุญาต ห้ามใช้เครื่องหมายดอกจัน
- ต่อ PC/SC แบบ SHARED รอด้วย SCardGetStatusChange แล้วตัดการเชื่อมต่อทันทีที่อ่านเสร็จ
- ถอด TIS-620 เองด้วยตารางตายตัว ไม่พึ่งโค้ดเพจของเครื่อง ฝังฟอนต์ไทยและประกาศ DPI ในไฟล์
- เซ็นไฟล์ด้วยใบรับรองสำหรับเซ็นโค้ดตั้งแต่รอบแรกที่จะเอาไปลงจริง อย่ารอให้โดนลบก่อนแล้วค่อยทำ
ส่วนตัวผมคิดว่างานประเภทสะพานเชื่อมเบราว์เซอร์กับฮาร์ดแวร์แบบนี้ เวลาที่ใช้จริงไม่ได้อยู่ที่โปรโตคอลของอุปกรณ์เลย โค้ดที่อ่านบัตรได้จริงมีไม่ถึงสองร้อยบรรทัด ที่เหลือคือการทำให้มันอยู่รอดในเครื่องของคนอื่น ที่มีโปรแกรมอื่นอยู่ก่อน มีนโยบายความปลอดภัยขององค์กรคุมอยู่ และมีคนใช้ที่ไม่ควรต้องมาเข้าใจว่า CORS คืออะไร ใครจะเอาโครงนี้ไปทำกับอุปกรณ์อื่นที่เบราว์เซอร์แตะไม่ได้ เช่นตาชั่งหรือเครื่องพิมพ์เฉพาะทาง โครงเดียวกันใช้ได้หมด เปลี่ยนแค่ชั้นที่คุยกับอุปกรณ์
ที่ยังค้างในแผนของผมคือทำตัวอัปเดตอัตโนมัติให้ agent เพราะตอนนี้เวลาแก้อะไรต้องไล่เดินไปทีละเคาน์เตอร์ กับย้ายไปใช้ WebSocket แทนการเรียกแบบถามครั้งเดียว เพื่อให้หน้าเว็บรู้ทันทีตอนมีคนเสียบบัตร โดยที่เจ้าหน้าที่ไม่ต้องกดปุ่มเลย ตรงไหนผมอธิบายตกหล่นหรือพิมพ์ผิด เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ
อยากให้เราทำให้ไหม?
ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า
เคยเจองานแบบนี้ไหม? ติดตรงไหน?
มาแลกวิธีกันต่อในบอร์ด LinuxThai



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