package hid import ( "context" "time" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" "NanoKVM-Server/proto" "NanoKVM-Server/service/inputcontrol" ) type Char struct { Modifiers int Code int } type PasteReq struct { Content string `form:"content" validate:"required"` Langue string `form:"langue"` } const ( defaultPasteDelay = 30 * time.Millisecond maxPasteDuration = 25 * time.Second maxPasteContentRunes = int(maxPasteDuration / defaultPasteDelay) ) func LangueSwitch(base map[rune]Char, lang string) map[rune]Char { // if no language is specified → return base map if lang == "" { return base } // always create a copy of the base map m := copyMap(base) switch lang { case "de": // swap Y m['y'] = Char{0, 29} m['Y'] = Char{2, 29} // swap Z m['z'] = Char{0, 28} m['Z'] = Char{2, 28} // add German special characters or remap them m['\u00E4'] = Char{0, 52} // ä m['\u00C4'] = Char{2, 52} // Ä m['\u00F6'] = Char{0, 51} // ö m['\u00D6'] = Char{2, 51} // Ö m['\u00FC'] = Char{0, 47} // ü m['\u00DC'] = Char{2, 47} // Ü m['\u00DF'] = Char{0, 45} // ß // swap special characters m['^'] = Char{0, 53} // must be double m['/'] = Char{2, 36} // Shift + 7 m['('] = Char{2, 37} // Shift + 8 m['&'] = Char{2, 35} // Shift + 6 m[')'] = Char{2, 38} // Shift + 9 m['`'] = Char{2, 46} // Grave Accent / Backtick m['"'] = Char{2, 31} // Shift + 2 m['?'] = Char{2, 45} // Shift + ß m['{'] = Char{0x40, 36} // ALt Gr + 7 m['['] = Char{0x40, 37} // ALt Gr + 8 m[']'] = Char{0x40, 38} // ALt Gr + 6 m['}'] = Char{0x40, 39} // ALt Gr + 0 m['\\'] = Char{0x40, 45} // ALt Gr + ß m['@'] = Char{0x40, 20} // ALt Gr + q m['+'] = Char{0, 48} // Shift + + m['*'] = Char{2, 48} // Shift + + m['~'] = Char{0x40, 48} // Shift + + m['#'] = Char{0, 49} // Shift + # m['\''] = Char{2, 49} // Shift + # m['<'] = Char{0, 100} // Shift + < m['>'] = Char{2, 100} // Shift + < m['|'] = Char{0x40, 100} // ALt Gr + < m[';'] = Char{2, 54} // Shift + , m[':'] = Char{2, 55} // Shift + . m['-'] = Char{0, 56} // Shift + - m['_'] = Char{2, 56} // Shift + - // new special characters m['\u00B4'] = Char{0, 46} // ´ m['\u00B0'] = Char{2, 53} // ° m['\u00A7'] = Char{2, 32} // § m['\u20AC'] = Char{0x40, 8} // € m['\u00B2'] = Char{0x40, 31} // ² m['\u00B3'] = Char{0x40, 32} // ³ case "fr": // French AZERTY layout // Letters: a↔q swap, z↔w swap, m moved to physical ; position m['a'] = Char{0, 20} // a is at physical Q key (HID 20) m['A'] = Char{2, 20} m['q'] = Char{0, 4} // q is at physical A key (HID 4) m['Q'] = Char{2, 4} m['z'] = Char{0, 26} // z is at physical W key (HID 26) m['Z'] = Char{2, 26} m['w'] = Char{0, 29} // w is at physical Z key (HID 29) m['W'] = Char{2, 29} m['m'] = Char{0, 51} // m is at physical ; key (HID 51) m['M'] = Char{2, 51} // Numbers require Shift on AZERTY m['1'] = Char{2, 30} m['2'] = Char{2, 31} m['3'] = Char{2, 32} m['4'] = Char{2, 33} m['5'] = Char{2, 34} m['6'] = Char{2, 35} m['7'] = Char{2, 36} m['8'] = Char{2, 37} m['9'] = Char{2, 38} m['0'] = Char{2, 39} // Unshifted number row → French/special characters m['&'] = Char{0, 30} // & at physical key 1 m['\u00E9'] = Char{0, 31} // é at physical key 2 m['"'] = Char{0, 32} // " at physical key 3 m['\''] = Char{0, 33} // ' at physical key 4 m['('] = Char{0, 34} // ( at physical key 5 m['-'] = Char{0, 35} // - at physical key 6 m['\u00E8'] = Char{0, 36} // è at physical key 7 m['_'] = Char{0, 37} // _ at physical key 8 m['\u00E7'] = Char{0, 38} // ç at physical key 9 m['\u00E0'] = Char{0, 39} // à at physical key 0 // Physical - key (HID 45) → ) on AZERTY m[')'] = Char{0, 45} // ) at physical - key m['\u00B0'] = Char{2, 45} // ° at shift+physical - key // Letter-row bracket/special keys m['^'] = Char{0, 47} // ^ (dead) at physical [ key (HID 47) m['\u00A8'] = Char{2, 47} // ¨ at shift+[ m['$'] = Char{0, 48} // $ at physical ] key (HID 48) m['\u00A3'] = Char{2, 48} // £ at shift+] m['*'] = Char{0, 49} // * at physical \ key (HID 49) m['\u00B5'] = Char{2, 49} // µ at shift+\ m['\u00F9'] = Char{0, 52} // ù at physical ' key (HID 52) m['%'] = Char{2, 52} // % at shift+' // Bottom row remappings m[','] = Char{0, 16} // , at physical M key (HID 16) m['?'] = Char{2, 16} // ? at shift+physical M m[';'] = Char{0, 54} // ; at physical , key (HID 54) m['.'] = Char{2, 54} // . at shift+physical , m[':'] = Char{0, 55} // : at physical . key (HID 55) m['/'] = Char{2, 55} // / at shift+physical . m['!'] = Char{0, 56} // ! at physical / key (HID 56) m['\u00A7'] = Char{2, 56} // § at shift+physical / // AltGr combinations m['~'] = Char{0x40, 31} // AltGr+2 m['#'] = Char{0x40, 32} // AltGr+3 m['{'] = Char{0x40, 33} // AltGr+4 m['['] = Char{0x40, 34} // AltGr+5 m['|'] = Char{0x40, 35} // AltGr+6 m['`'] = Char{0x40, 36} // AltGr+7 m['\\'] = Char{0x40, 37} // AltGr+8 m[']'] = Char{0x40, 45} // AltGr+physical - m['}'] = Char{0x40, 46} // AltGr+= m['@'] = Char{0x40, 39} // AltGr+0 m['\u20AC'] = Char{0x40, 8} // € AltGr+E } return m } func (s *Service) Paste(c *gin.Context) { var req PasteReq var rsp proto.Response if err := proto.ParseFormRequest(c, &req); err != nil { rsp.ErrRsp(c, -1, "invalid arguments") return } contentRunes := []rune(req.Content) if len(contentRunes) > maxPasteContentRunes { rsp.ErrRsp(c, -2, "content too long") return } charMapLocal := LangueSwitch(charMap, req.Langue) typeableRunes := 0 for _, char := range contentRunes { if _, ok := charMapLocal[char]; ok { typeableRunes++ } } if time.Duration(typeableRunes)*defaultPasteDelay > maxPasteDuration { rsp.ErrRsp(c, -2, "paste duration exceeds 25s") return } keyUp := []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} manual := s.newManualSession() defer manual.Close() reservation, err := manual.Reserve(c.Request.Context(), inputcontrol.ManualKeyboard, false, nil) if err != nil { log.Errorf("manual paste failed to acquire HID control: %v", err) rsp.ErrRsp(c, -3, "HID control is busy") return } writeKeyboardReport := func(report []byte) error { return manual.Execute(func() error { return s.hid.WriteKeyboardReport(report) }) } for _, char := range contentRunes { if err := context.Cause(c.Request.Context()); err != nil { break } key, ok := charMapLocal[char] if !ok { log.Debugf("unknown key '%c' (rune: %d)", char, char) continue } keyDown := []byte{byte(key.Modifiers), 0x00, byte(key.Code), 0x00, 0x00, 0x00, 0x00, 0x00} if err = writeKeyboardReport(keyDown); err != nil { break } if err = writeKeyboardReport(keyUp); err != nil { break } if err = sleepPasteContext(c.Request.Context(), defaultPasteDelay); err != nil { break } } if err == nil { err = context.Cause(c.Request.Context()) } _ = writeKeyboardReport(keyUp) reservation.Complete(err == nil) if err != nil { log.Errorf("hid paste failed: %v", err) rsp.ErrRsp(c, -3, "HID paste failed") return } rsp.OkRsp(c) log.Debugf("hid paste success, total %d characters processed", len(contentRunes)) } func sleepPasteContext(ctx context.Context, delay time.Duration) error { timer := time.NewTimer(delay) defer timer.Stop() select { case <-ctx.Done(): return context.Cause(ctx) case <-timer.C: return nil } } func copyMap(src map[rune]Char) map[rune]Char { dst := make(map[rune]Char, len(src)) for k, v := range src { dst[k] = v } return dst } func GetCharMap(lang string) map[rune]Char { return LangueSwitch(charMap, lang) } var charMap = map[rune]Char{ // Lowercase letters 'a': {0, 4}, 'b': {0, 5}, 'c': {0, 6}, 'd': {0, 7}, 'e': {0, 8}, 'f': {0, 9}, 'g': {0, 10}, 'h': {0, 11}, 'i': {0, 12}, 'j': {0, 13}, 'k': {0, 14}, 'l': {0, 15}, 'm': {0, 16}, 'n': {0, 17}, 'o': {0, 18}, 'p': {0, 19}, 'q': {0, 20}, 'r': {0, 21}, 's': {0, 22}, 't': {0, 23}, 'u': {0, 24}, 'v': {0, 25}, 'w': {0, 26}, 'x': {0, 27}, 'y': {0, 28}, 'z': {0, 29}, // Uppercase letters (Modifier 2 typically means Left Shift) 'A': {2, 4}, 'B': {2, 5}, 'C': {2, 6}, 'D': {2, 7}, 'E': {2, 8}, 'F': {2, 9}, 'G': {2, 10}, 'H': {2, 11}, 'I': {2, 12}, 'J': {2, 13}, 'K': {2, 14}, 'L': {2, 15}, 'M': {2, 16}, 'N': {2, 17}, 'O': {2, 18}, 'P': {2, 19}, 'Q': {2, 20}, 'R': {2, 21}, 'S': {2, 22}, 'T': {2, 23}, 'U': {2, 24}, 'V': {2, 25}, 'W': {2, 26}, 'X': {2, 27}, 'Y': {2, 28}, 'Z': {2, 29}, // Numbers '1': {0, 30}, '2': {0, 31}, '3': {0, 32}, '4': {0, 33}, '5': {0, 34}, '6': {0, 35}, '7': {0, 36}, '8': {0, 37}, '9': {0, 38}, '0': {0, 39}, // Shifted numbers / Symbols '!': {2, 30}, // Shift + 1 '@': {2, 31}, // Shift + 2 '#': {2, 32}, // Shift + 3 '$': {2, 33}, // Shift + 4 '%': {2, 34}, // Shift + 5 '^': {2, 35}, // Shift + 6 '&': {2, 36}, // Shift + 7 '*': {2, 37}, // Shift + 8 '(': {2, 38}, // Shift + 9 ')': {2, 39}, // Shift + 0 // Other common characters '\n': {0, 40}, // Enter (Return) '\t': {0, 43}, // Tab ' ': {0, 44}, // Space '-': {0, 45}, // Hyphen / Minus '=': {0, 46}, // Equals '[': {0, 47}, // Left Square Bracket ']': {0, 48}, // Right Square Bracket '\\': {0, 49}, // Backslash ';': {0, 51}, // Semicolon '\'': {0, 52}, // Apostrophe / Single Quote '`': {0, 53}, // Grave Accent / Backtick ',': {0, 54}, // Comma '.': {0, 55}, // Period / Dot '/': {0, 56}, // Slash // Shifted symbols '_': {2, 45}, // Underscore (Shift + Hyphen) '+': {2, 46}, // Plus (Shift + Equals) '{': {2, 47}, // Left Curly Brace (Shift + Left Square Bracket) '}': {2, 48}, // Right Curly Brace (Shift + Right Square Bracket) '|': {2, 49}, // Pipe (Shift + Backslash) ':': {2, 51}, // Colon (Shift + Semicolon) '"': {2, 52}, // Double Quote (Shift + Apostrophe) '~': {2, 53}, // Tilde (Shift + Grave Accent) '<': {2, 54}, // Less Than (Shift + Comma) '>': {2, 55}, // Greater Than (Shift + Period) '?': {2, 56}, // Question Mark (Shift + Slash) }