// +build windows package sys_windows import "core:strings" import "core:sys/win32" LOWORD :: #force_inline proc "contextless" (x: DWORD) -> WORD { return WORD(x & 0xffff); } HIWORD :: #force_inline proc "contextless" (x: DWORD) -> WORD { return WORD(x >> 16); } utf8_to_utf16 :: proc(s: string, allocator := context.temp_allocator) -> []u16 { if len(s) < 1 { return nil; } b := transmute([]byte)s; cstr := raw_data(b); n := MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, cstr, i32(len(s)), nil, 0); if n == 0 { return nil; } text := make([]u16, n+1, allocator); n1 := MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, cstr, i32(len(s)), raw_data(text), n); if n1 == 0 { delete(text, allocator); return nil; } text[n] = 0; for n >= 1 && text[n-1] == 0 { n -= 1; } return text[:n]; } utf8_to_wstring :: proc(s: string, allocator := context.temp_allocator) -> wstring { if res := utf8_to_utf16(s, allocator); res != nil { return &res[0]; } return nil; } wstring_to_utf8 :: proc(s: wstring, N: int, allocator := context.temp_allocator) -> string { if N <= 0 { return ""; } n := WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, s, i32(N), nil, 0, nil, nil); if n == 0 { return ""; } // If N == -1 the call to WideCharToMultiByte assume the wide string is null terminated // and will scan it to find the first null terminated character. The resulting string will // also null terminated. // If N != -1 it assumes the wide string is not null terminated and the resulting string // will not be null terminated, we therefore have to force it to be null terminated manually. text := make([]byte, n+1 if N != -1 else n, allocator); n1 := WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, s, i32(N), raw_data(text), n, nil, nil); if n1 == 0 { delete(text, allocator); return ""; } for i in 0.. string { if len(s) == 0 { return ""; } return wstring_to_utf8(raw_data(s), len(s), allocator); } // AdvAPI32, NetAPI32 and UserENV helpers. allowed_username :: proc(username: string) -> bool { /* User account names are limited to 20 characters and group names are limited to 256 characters. In addition, account names cannot be terminated by a period and they cannot include commas or any of the following printable characters: ", /, , [, ], :, |, <, >, +, =, ;, ?, *. Names also cannot include characters in the range 1-31, which are nonprintable. */ _DISALLOWED :: "\"/ []:|<>+=;?*,"; if len(username) > LM20_UNLEN || len(username) == 0 { return false; } if username[len(username)-1] == '.' { return false; } for r in username { if r > 0 && r < 32 { return false; } } if strings.contains_any(username, _DISALLOWED) { return false; } return true; } // Returns .Success on success. _add_user :: proc(servername: string, username: string, password: string) -> (ok: NET_API_STATUS) { servername_w: wstring; username_w: []u16; password_w: []u16; if len(servername) == 0 { // Create account on this computer servername_w = nil; } else { server := utf8_to_utf16(servername, context.temp_allocator); servername_w = &server[0]; } if len(username) == 0 || len(username) > LM20_UNLEN { return .BadUsername; } if !allowed_username(username) { return .BadUsername; } if len(password) == 0 || len(password) > LM20_PWLEN { return .BadPassword; } username_w = utf8_to_utf16(username, context.temp_allocator); password_w = utf8_to_utf16(password, context.temp_allocator); level := DWORD(1); parm_err: DWORD; user_info := USER_INFO_1{ name = &username_w[0], password = &password_w[0], // Max password length is defined in LM20_PWLEN. password_age = 0, // Ignored priv = .User, home_dir = nil, // We'll set it later comment = nil, flags = {.Script, .Normal_Account}, script_path = nil, }; ok = NetUserAdd( servername_w, level, &user_info, &parm_err, ); return; } get_computer_name_and_account_sid :: proc(username: string) -> (computer_name: string, sid := SID{}, ok: bool) { username_w := utf8_to_utf16(username, context.temp_allocator); cbsid: DWORD; computer_name_size: DWORD; pe_use := SID_TYPE.User; res := LookupAccountNameW( nil, // Look on this computer first &username_w[0], &sid, &cbsid, nil, &computer_name_size, &pe_use, ); if computer_name_size == 0 { // User didn't exist, or we'd have a size here. return "", {}, false; } cname_w := make([]u16, min(computer_name_size, 1), context.temp_allocator); res = LookupAccountNameW( nil, &username_w[0], &sid, &cbsid, &cname_w[0], &computer_name_size, &pe_use, ); if !res { return "", {}, false; } computer_name = utf16_to_utf8(cname_w, context.temp_allocator); ok = true; return; } get_sid :: proc(username: string, sid: ^SID) -> (ok: bool) { username_w := utf8_to_utf16(username, context.temp_allocator); cbsid: DWORD; computer_name_size: DWORD; pe_use := SID_TYPE.User; res := LookupAccountNameW( nil, // Look on this computer first &username_w[0], sid, &cbsid, nil, &computer_name_size, &pe_use, ); if computer_name_size == 0 { // User didn't exist, or we'd have a size here. return false; } cname_w := make([]u16, min(computer_name_size, 1), context.temp_allocator); res = LookupAccountNameW( nil, &username_w[0], sid, &cbsid, &cname_w[0], &computer_name_size, &pe_use, ); if !res { return false; } ok = true; return; } add_user_to_group :: proc(sid: ^SID, group: string) -> (ok: NET_API_STATUS) { group_member := LOCALGROUP_MEMBERS_INFO_0{ sid = sid, }; group_name := utf8_to_utf16(group, context.temp_allocator); ok = NetLocalGroupAddMembers( nil, &group_name[0], 0, &group_member, 1, ); return; } add_del_from_group :: proc(sid: ^SID, group: string) -> (ok: NET_API_STATUS) { group_member := LOCALGROUP_MEMBERS_INFO_0{ sid = sid, }; group_name := utf8_to_utf16(group, context.temp_allocator); ok = NetLocalGroupDelMembers( nil, &group_name[0], 0, &group_member, 1, ); return; } add_user_profile :: proc(username: string) -> (ok: bool, profile_path: string) { username_w := utf8_to_utf16(username, context.temp_allocator); sid := SID{}; ok = get_sid(username, &sid); if ok == false { return false, ""; } sb: wstring; res := ConvertSidToStringSidW(&sid, &sb); if res == false { return false, ""; } defer win32.local_free(sb); pszProfilePath := make([]u16, 257, context.temp_allocator); res2 := CreateProfile( sb, &username_w[0], &pszProfilePath[0], 257, ); if res2 != 0 { return false, ""; } profile_path = wstring_to_utf8(&pszProfilePath[0], 257); return true, profile_path; } delete_user_profile :: proc(username: string) -> (ok: bool) { sid := SID{}; ok = get_sid(username, &sid); if ok == false { return false; } sb: wstring; res := ConvertSidToStringSidW(&sid, &sb); if res == false { return false; } defer win32.local_free(sb); res2 := DeleteProfileW( sb, nil, nil, ); return bool(res2); } add_user :: proc(servername: string, username: string, password: string) -> (ok: bool) { /* Convenience function that creates a new user, adds it to the group Users and creates a profile directory for it. Requires elevated privileges (run as administrator). TODO: Add a bool that governs whether to delete the user if adding to group and/or creating profile fail? TODO: SecureZeroMemory the password after use. */ res := _add_user(servername, username, password); if res != .Success { return false; } // Grab the SID to add the user to the Users group. sid: SID; ok2 := get_sid(username, &sid); if ok2 == false { return false; } ok3 := add_user_to_group(&sid, "Users"); if ok3 != .Success { return false; } return true; } delete_user :: proc(servername: string, username: string) -> (ok: bool) { /* Convenience function that deletes a user. Requires elevated privileges (run as administrator). TODO: Add a bool that governs whether to delete the profile from this wrapper? */ servername_w: wstring; if len(servername) == 0 { // Delete account on this computer servername_w = nil; } else { server := utf8_to_utf16(servername, context.temp_allocator); servername_w = &server[0]; } username_w := utf8_to_utf16(username); res := NetUserDel( servername_w, &username_w[0], ); if res != .Success { return false; } return true; } run_as_user :: proc(username, password, application, commandline: string, pi: ^PROCESS_INFORMATION, wait := true) -> (ok: bool) { /* Needs to be run as an account which has the "Replace a process level token" privilege. This can be added to an account from: Control Panel -> Administrative Tools -> Local Security Policy. The path to this policy is as follows: Local Policies -> User Rights Assignment -> Replace a process level token. A reboot may be required for this change to take effect and impersonating a user to work. TODO: SecureZeroMemory the password after use. */ username_w := utf8_to_utf16(username); domain_w := utf8_to_utf16("."); password_w := utf8_to_utf16(password); app_w := utf8_to_utf16(application); commandline_w: []u16 = {0}; if len(commandline) > 0 { commandline_w = utf8_to_utf16(commandline); } user_token: HANDLE; ok = bool(LogonUserW( lpszUsername = &username_w[0], lpszDomain = &domain_w[0], lpszPassword = &password_w[0], dwLogonType = .NEW_CREDENTIALS, dwLogonProvider = .WINNT50, phToken = &user_token, )); if !ok { return false; // err := GetLastError(); // fmt.printf("GetLastError: %v\n", err); } si := STARTUPINFO{}; si.cb = size_of(STARTUPINFO); pi := pi; ok = bool(CreateProcessAsUserW( user_token, &app_w[0], &commandline_w[0], nil, // lpProcessAttributes, nil, // lpThreadAttributes, false, // bInheritHandles, 0, // creation flags nil, // environment, nil, // current directory: inherit from parent if nil &si, pi, )); if ok { if wait { WaitForSingleObject(pi.hProcess, INFINITE); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); } return true; } else { return false; } }