Skip to content

Serwer ​

Poniżej kompletny serwer w czterech językach: sprawdza login i hasło, zapisuje odbicie do pliku i dopiero wtedy odpowiada 200, rozpoznaje duplikaty i odbicia z nieustawionym zegarem. Każdy ma kilkadziesiąt linii i nie wymaga żadnych bibliotek.

py
#!/usr/bin/env python3
# Minimal RAZ-RCP-1 punch receiver (Python 3.9+, standard library only).
# Reader: Settings > Integrations > Synchronization, format http3,
# URL http://<this computer>:8080/odbicia, login rcp, password = RCP_HASLO.
import base64, hmac, os, re
from datetime import datetime, timezone
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from threading import Lock
from urllib.parse import parse_qs

LOGIN, HASLO = "rcp", os.environ["RCP_HASLO"]
FILE, lock, seen = "odbicia.tsv", Lock(), set()

class Handler(BaseHTTPRequestHandler):
    def answer(self, code, body):
        self.send_response(code)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body.encode())

    def do_POST(self):
        auth = "Basic " + base64.b64encode(f"{LOGIN}:{HASLO}".encode()).decode()
        if not hmac.compare_digest(self.headers.get("Authorization", ""), auth):
            return self.answer(401, '{"error":"unauthorized"}')    # punch stays in the reader
        n = int(self.headers.get("Content-Length") or 0)
        if n > 4096:
            self.close_connection = True
            return self.answer(400, '{"error":"too large"}')
        form = parse_qs(self.rfile.read(n).decode("utf-8", "replace"))
        rfid = form.get("rfidID", [""])[0]
        machine = re.sub(r"[\t\r\n]", " ", form.get("machineId", [""])[0])
        try:
            ts = datetime.strptime(form.get("timestamp", [""])[0], "%Y-%m-%d %H:%M:%S")
        except ValueError:
            ts = None
        if not re.fullmatch(r"[0-9a-f]{1,20}", rfid) or ts is None:
            return self.answer(400, '{"error":"bad request"}')     # dropped by the reader
        epoch = int(ts.replace(tzinfo=timezone.utc).timestamp())  # http3: UTC
        if ts.year < 2026:
            return self.answer(200, '{"stored":false,"reason":"clock"}')
        key = (epoch, rfid, machine)
        with lock:
            if key in seen:
                return self.answer(200, '{"stored":false,"reason":"duplicate"}')
            with open(FILE, "a", encoding="utf-8") as f:            # store first...
                f.write(f"{epoch}\t{rfid}\t{machine}\n")
                f.flush()
                os.fsync(f.fileno())
            seen.add(key)
        self.answer(200, '{"stored":true}')                         # ...then answer 200

ThreadingHTTPServer(("0.0.0.0", 8080), Handler).serve_forever()
js
// Minimal RAZ-RCP-1 punch receiver (Node.js 18+, no dependencies).
// Reader: Settings > Integrations > Synchronization, format http3,
// URL http://<this computer>:8080/odbicia, login rcp, password = RCP_HASLO.
const http = require("node:http");
const fs = require("node:fs");
const crypto = require("node:crypto");

const LOGIN = "rcp", HASLO = process.env.RCP_HASLO, FILE = "odbicia.tsv";
const seen = new Set();
const expected = Buffer.from("Basic " + Buffer.from(`${LOGIN}:${HASLO}`).toString("base64"));

function answer(res, code, body) {
  res.writeHead(code, { "Content-Type": "application/json" });
  res.end(body);
}

http.createServer((req, res) => {
  const got = Buffer.from(req.headers.authorization || "");
  if (got.length !== expected.length || !crypto.timingSafeEqual(got, expected)) {
    return answer(res, 401, '{"error":"unauthorized"}'); // punch stays in the reader
  }
  let raw = "";
  req.on("data", (chunk) => { if (raw.length < 4096) raw += chunk; });
  req.on("end", () => {
    const form = new URLSearchParams(raw);
    const rfid = form.get("rfidID") || "", machine = form.get("machineId") || "";
    const m = /^(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)$/.exec(form.get("timestamp") || "");
    if (!/^[0-9a-f]{1,20}$/.test(rfid) || !m) {
      return answer(res, 400, '{"error":"bad request"}'); // dropped by the reader
    }
    const epoch = Date.UTC(m[1], m[2] - 1, m[3], m[4], m[5], m[6]) / 1000; // http3: UTC
    if (Number(m[1]) < 2026) return answer(res, 200, '{"stored":false,"reason":"clock"}');
    const key = `${epoch}\t${rfid}\t${machine}`;
    if (seen.has(key)) return answer(res, 200, '{"stored":false,"reason":"duplicate"}');
    const fd = fs.openSync(FILE, "a");                  // store first...
    fs.writeSync(fd, key.replace(/[\r\n]/g, " ") + "\n");
    fs.fsyncSync(fd);
    fs.closeSync(fd);
    seen.add(key);
    answer(res, 200, '{"stored":true}');                // ...then answer 200
  });
}).listen(8080);
go
// Minimal RAZ-RCP-1 punch receiver (Go 1.21+, standard library only).
// Reader: Settings > Integrations > Synchronization, format http3,
// URL http://<this computer>:8080/odbicia, login rcp, password = RCP_HASLO.
package main

import (
	"crypto/subtle"
	"fmt"
	"net/http"
	"os"
	"regexp"
	"strings"
	"sync"
	"time"
)

var (
	login, haslo = "rcp", os.Getenv("RCP_HASLO")
	rfidRe       = regexp.MustCompile(`^[0-9a-f]{1,20}$`)
	mu           sync.Mutex
	seen         = map[string]bool{}
)

func answer(w http.ResponseWriter, code int, body string) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(code)
	fmt.Fprint(w, body)
}

func punch(w http.ResponseWriter, r *http.Request) {
	user, pass, _ := r.BasicAuth()
	if subtle.ConstantTimeCompare([]byte(user+":"+pass), []byte(login+":"+haslo)) != 1 {
		answer(w, 401, `{"error":"unauthorized"}`) // punch stays in the reader
		return
	}
	r.Body = http.MaxBytesReader(w, r.Body, 4096)
	rfid, machine := r.FormValue("rfidID"), r.FormValue("machineId")
	ts, err := time.Parse("2006-01-02 15:04:05", r.FormValue("timestamp")) // http3: UTC
	if !rfidRe.MatchString(rfid) || err != nil {
		answer(w, 400, `{"error":"bad request"}`) // dropped by the reader
		return
	}
	if ts.Year() < 2026 {
		answer(w, 200, `{"stored":false,"reason":"clock"}`)
		return
	}
	key := fmt.Sprintf("%d\t%s\t%s", ts.Unix(), rfid, strings.NewReplacer("\r", " ", "\n", " ").Replace(machine))
	mu.Lock()
	defer mu.Unlock()
	if seen[key] {
		answer(w, 200, `{"stored":false,"reason":"duplicate"}`)
		return
	}
	f, err := os.OpenFile("odbicia.tsv", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600) // store first...
	if err == nil {
		_, err = f.WriteString(key + "\n")
		if err == nil {
			err = f.Sync()
		}
		f.Close()
	}
	if err != nil {
		answer(w, 500, `{"error":"storage"}`) // the reader retries later
		return
	}
	seen[key] = true
	answer(w, 200, `{"stored":true}`) // ...then answer 200
}

func main() {
	http.HandleFunc("POST /odbicia", punch)
	http.ListenAndServe(":8080", nil)
}
php
<?php
// Minimal RAZ-RCP-1 punch receiver (PHP 8.1+). Run: RCP_HASLO=secret php -S 0.0.0.0:8080 index.php
// Reader: Settings > Integrations > Synchronization, format http3,
// URL http://<this computer>:8080/odbicia, login rcp, password = RCP_HASLO.
// Under Apache/nginx keep odbicia.tsv outside the web root.
$login = 'rcp';
$haslo = getenv('RCP_HASLO');
$file = __DIR__ . '/odbicia.tsv';

function answer(int $code, string $body): never {
    http_response_code($code);
    header('Content-Type: application/json');
    exit($body);
}

$given = ($_SERVER['PHP_AUTH_USER'] ?? '') . ':' . ($_SERVER['PHP_AUTH_PW'] ?? '');
if (!hash_equals("$login:$haslo", $given)) {
    answer(401, '{"error":"unauthorized"}');        // punch stays in the reader
}
$rfid = $_POST['rfidID'] ?? '';
$machine = str_replace(["\r", "\n", "\t"], ' ', $_POST['machineId'] ?? '');
$ts = DateTimeImmutable::createFromFormat('!Y-m-d H:i:s', $_POST['timestamp'] ?? '', new DateTimeZone('UTC'));
if (!preg_match('/^[0-9a-f]{1,20}$/', $rfid) || !$ts) {
    answer(400, '{"error":"bad request"}');         // dropped by the reader
}
if ((int)$ts->format('Y') < 2026) {
    answer(200, '{"stored":false,"reason":"clock"}');
}
$line = $ts->getTimestamp() . "\t$rfid\t$machine\n";

$f = fopen($file, 'c+');
flock($f, LOCK_EX);                                 // one request at a time
while (($l = fgets($f)) !== false) {
    if ($l === $line) {
        answer(200, '{"stored":false,"reason":"duplicate"}');
    }
}
fwrite($f, $line);                                  // store first...
fflush($f);
fsync($f);
answer(200, '{"stored":true}');                     // ...then answer 200 (exit releases the lock)

Uruchomienie (hasło ustawiasz takie samo jak w czytniku):

bash
RCP_HASLO=tajne python3 server.py
bash
RCP_HASLO=tajne node server.js
bash
RCP_HASLO=tajne go run main.go
bash
RCP_HASLO=tajne php -S 0.0.0.0:8080 index.php

Serwer słucha na porcie 8080 i dopisuje odbicia do odbicia.tsv (epoch<TAB>rfidID<TAB>machineId).

Rozbudowane przykłady w sześciu językach (Python, Go, Node.js, Java, C#, PHP), z raportem czasu pracy i listą pracowników, są w katalogu examples/RAZ-RCP-1/http.