1. Detecting shares using walk_dir()

Hi,

I'm trying to create a program that logs files on certain servers, but I'd like it to automatically detect the (public) shares.

Example;
some-server\<arbitrary public shares>\

I'm currently using walk_dir() to log the files, but it wont accept;

object nResult = walk_dir("\\some-server\", routine_id("myWDRoutine"), true ) 

The above code results in no output, even if there where shares for that server. Is there another way I can solve this?

Kenneth aka ZNorQ

new topic     » topic index » view message » categorize

2. Re: Detecting shares using walk_dir()

What happens when you map a network drive? Unfortunately I have nothing to test it on here.

Erm, belay that, can you explain/expand that example? What are you actually doing there, as in an example terminal session. What OS are you on?

Pete

new topic     » goto parent     » topic index » view message » categorize

3. Re: Detecting shares using walk_dir()

This code works for me. Make sure you're properly escaping your backslashes as shown below (in your example you are not).

include std/console.e 
include std/filesys.e 
 
function look_at( sequence path, sequence item ) 
 
    display( item[D_NAME] ) 
 
    return 0 
end function 
 
walk_dir( "\\\\localhost\\c$", routine_id("look_at") ) 
new topic     » goto parent     » topic index » view message » categorize

4. Re: Detecting shares using walk_dir()

ghaberek said...

This code works for me. Make sure you're properly escaping your backslashes as shown below (in your example you are not).

include std/console.e 
include std/filesys.e 
 
function look_at( sequence path, sequence item ) 
 
    display( item[D_NAME] ) 
 
    return 0 
end function 
 
walk_dir( "\\\\localhost\\c$", routine_id("look_at") ) 

Not sure this helps .. the OP wants to get the list of shares on a machine, not look inside a specific share. Something akin to "smbclient -L machine" IIUC.

petelomax said...

What OS are you on?

Again, I'm not sure if this helps, but it's probably safe to say that the OP is using some version of windoze.

new topic     » goto parent     » topic index » view message » categorize

5. Re: Detecting shares using walk_dir()

jimcbrown said...

Not sure this helps .. the OP wants to get the list of shares on a machine, not look inside a specific share. Something akin to "smbclient -L machine" IIUC.

Yeah, I kind of overshot that one, didn't I? I saw the mis-escaped backslashes and figured that was his problem. getlost

I believe the trick here is to use WNetEnumResource. Here is a wrapper for the necessary API functions to get started. I will actually wrap them into a better "enumerate shares" function if I have time later.

Also, I should point out that walk_dir() and other functions do not work here, because the resource \\some-server is not a directory. It is a "container" and the shares inside may point to either a disk or a printer. So we're concerned about enumerating the disk shares, which we can then inspect with walk_dir(), etc.

-- file: mpr.e 
include std/dll.e 
include std/machine.e 
include std/math.e 
 
public constant 
    RESOURCE_CONNECTED  = 1, 
    RESOURCE_GLOBALNET  = 2, 
    RESOURCE_REMEMBERED = 3, 
    RESOURCE_RECENT     = 4, 
    RESOURCE_CONTEXT    = 5, 
$ 
 
public constant 
    RESOURCETYPE_ANY        = 0, 
    RESOURCETYPE_DISK       = 1, 
    RESOURCETYPE_PRINT      = 2, 
    RESOURCETYPE_RESERVED   = 8, 
    RESOURCETYPE_UNKNOWN    = #FFFFFFFF, 
$ 
 
public constant 
    RESOURCEUSAGE_CONNECTABLE   = #00000001, 
    RESOURCEUSAGE_CONTAINER     = #00000002, 
    RESOURCEUSAGE_NOLOCALDEVICE = #00000004, 
    RESOURCEUSAGE_SIBLING       = #00000008, 
    RESOURCEUSAGE_ATTACHED      = #00000010, 
    RESOURCEUSAGE_ALL           = or_all({ RESOURCEUSAGE_CONNECTABLE, RESOURCEUSAGE_CONTAINER, RESOURCEUSAGE_ATTACHED }), 
    RESOURCEUSAGE_RESERVED      = #80000000, 
$ 
 
public constant 
    RESOURCEDISPLAYTYPE_GENERIC     =  0, 
    RESOURCEDISPLAYTYPE_DOMAIN      =  1, 
    RESOURCEDISPLAYTYPE_SERVER      =  2, 
    RESOURCEDISPLAYTYPE_SHARE       =  3, 
    RESOURCEDISPLAYTYPE_FILE        =  4, 
    RESOURCEDISPLAYTYPE_GROUP       =  5, 
    RESOURCEDISPLAYTYPE_NETWORK     =  6, 
    RESOURCEDISPLAYTYPE_ROOT        =  7, 
    RESOURCEDISPLAYTYPE_SHAREADMIN  =  8, 
    RESOURCEDISPLAYTYPE_DIRECTORY   =  9, 
    RESOURCEDISPLAYTYPE_TREE        = 10, 
$ 
 
public constant -- structure 
    NETRESOURCE__dwScope        =  0, -- DWORD 
    NETRESOURCE__dwType         =  4, -- DWORD 
    NETRESOURCE__dwDisplayType  =  8, -- DWORD 
    NETRESOURCE__dwUsage        = 12, -- DWORD 
    NETRESOURCE__lpLocalName    = 16, -- LPTSTR 
    NETRESOURCE__lpRemoteName   = 20, -- LPTSTR 
    NETRESOURCE__lpComment      = 24, -- LPTSTR 
    NETRESOURCE__lpProvider     = 28, -- LPTSTR 
    SIZEOF_NETRESOURCE          = 32, 
$ 
 
public atom mpr_dll = open_dll( "mpr.dll" ) 
 
public constant 
    xWNetCloseEnum      = define_c_func( mpr_dll, "WNetCloseEnum", {C_HANDLE}, C_DWORD ), 
    xWNetEnumResource   = define_c_func( mpr_dll, "WNetEnumResourceA", {C_HANDLE,C_POINTER,C_POINTER,C_POINTER}, C_DWORD ), 
    xWNetOpenEnum       = define_c_func( mpr_dll, "WNetOpenEnumA", {C_DWORD,C_DWORD,C_DWORD,C_POINTER,C_POINTER}, C_DWORD ), 
$ 
 
public function WNetCloseEnum( atom hEnum ) 
    return c_func( xWNetCloseEnum, {hEnum} ) 
end function 
 
public function WNetEnumResource( atom hEnum, atom lpcCount, atom lpBuffer, atom lpBufferSize ) 
    return c_func( xWNetEnumResource, {hEnum,lpcCount,lpBuffer,lpBufferSize} ) 
end function 
 
public function WNetOpenEnum( atom dwScope, atom dwType, atom dwUsage, atom lpNetResource, atom lphEnum ) 
    return c_func( xWNetOpenEnum, {dwScope,dwType,dwUsage,lpNetResource,lphEnum} ) 
end function 

-Greg

new topic     » goto parent     » topic index » view message » categorize

6. Re: Detecting shares using walk_dir()

petelomax said...

What happens when you map a network drive? Unfortunately I have nothing to test it on here.

Erm, belay that, can you explain/expand that example? What are you actually doing there, as in an example terminal session. What OS are you on?

Pete

The point of the program is not to map a network drive, but to walk through all public shares (and folders/files) on certain servers; so I know the server names (i.e. "someserver", "someotherserver", etc.), but the (public) shares on each server may vary. Over time there might be some new shares, some shares are dropped, etc.

Oh, and yes, it is Windows.

PS! I know the server names starts with backslash-backslash, but the Creole for this seems to be newline, and I'm not sure how to override the Creole..

Regards Kenneth aka ZNorQ.

new topic     » goto parent     » topic index » view message » categorize

7. Re: Detecting shares using walk_dir()

jimcbrown said...
ghaberek said...

This code works for me. Make sure you're properly escaping your backslashes as shown below (in your example you are not).

include std/console.e 
include std/filesys.e 
 
function look_at( sequence path, sequence item ) 
 
    display( item[D_NAME] ) 
 
    return 0 
end function 
 
walk_dir( "\\\\localhost\\c$", routine_id("look_at") ) 

Not sure this helps .. the OP wants to get the list of shares on a machine, not look inside a specific share. Something akin to "smbclient -L machine" IIUC.

petelomax said...

What OS are you on?

Again, I'm not sure if this helps, but it's probably safe to say that the OP is using some version of windoze.

I know the server names, I don't know the shares. Over time they may even be changed (new added, old removed, etc)

And sorry, yes, it is windows (forgot to mention that in my post).

Kenneth aka ZNorQ.

new topic     » goto parent     » topic index » view message » categorize

8. Re: Detecting shares using walk_dir()

ghaberek said...
jimcbrown said...

Not sure this helps .. the OP wants to get the list of shares on a machine, not look inside a specific share. Something akin to "smbclient -L machine" IIUC.

Yeah, I kind of overshot that one, didn't I? I saw the mis-escaped backslashes and figured that was his problem. getlost

I believe the trick here is to use WNetEnumResource. Here is a wrapper for the necessary API functions to get started. I will actually wrap them into a better "enumerate shares" function if I have time later.

Also, I should point out that walk_dir() and other functions do not work here, because the resource \\some-server is not a directory. It is a "container" and the shares inside may point to either a disk or a printer. So we're concerned about enumerating the disk shares, which we can then inspect with walk_dir(), etc.

<eucode> snipped

-Greg

ghaberek said...

"Also, I should point out that walk_dir() and other functions do not work here, because the resource \\some-server is not a directory. It is a "container" and the shares inside may point to either a disk or a printer."

Wouldn't it be a good addition to the walk_dir() function to detect and process the public shares (and ignore the printers) if possible?

When it comes to windows-functionality, I'm pretty weak. I'm learning, but I wouldn't know where to start using the code above.

Kenneth aka ZNorQ.

PS! Thanks for the great feedback!

new topic     » goto parent     » topic index » view message » categorize

9. Re: Detecting shares using walk_dir()

ZNorQ said...

Wouldn't it be a good addition to the walk_dir() function to detect and process the public shares (and ignore the printers) if possible?

No. As ghaberek already explained,

ghaberek said...

"Also, I should point out that walk_dir() and other functions do not work here, because the resource \\some-server is not a directory. It is a "container"

So imvho this functionality is better off living in the enumerate_shares() function that ghaberek will hopefully get around to writing soon.

new topic     » goto parent     » topic index » view message » categorize

10. Re: Detecting shares using walk_dir()

jimcbrown said...

So imvho this functionality is better off living in the enumerate_shares() function that ghaberek will hopefully get around to writing soon.

And here it is! smile

You can use scan_network() to scan the network for available servers and/or call enumerate_shares() against a specific host (e.g. \\some-server).

This won't display any hidden shares (like C$ or admin$) because, well... they're hidden. But you can still use walk_dir() against those shares.

-- file: shares.e 
 
include std/dll.e 
include std/machine.e 
include mpr.e -- WNet* functions and constants 
 
constant KB = 1024 -- one kilobyte 
 
public constant 
    -- possible results codes 
    NO_ERROR                    =    0, 
    ERROR_INVALID_HANDLE        =    6, 
    ERROR_INVALID_PARAMETER     =   87, 
    ERROR_MORE_DATA             =  234, 
    ERROR_NO_MORE_ITEMS         =  259, 
    ERROR_INVALID_ADDRESS       =  487, 
    ERROR_NO_NET_OR_BAD_PATH    = 1203, 
    ERROR_NOT_CONTAINER         = 1207, 
    ERROR_EXTENDED_ERROR        = 1208, 
    ERROR_NO_NETWORK            = 1222, 
$ 
 
public enum 
    -- values returned by get_NETRESOURCE() 
    NETRESOURCE_SCOPE, 
    NETRESOURCE_TYPE, 
    NETRESOURCE_DISPLAY_TYPE, 
    NETRESOURCE_USAGE, 
    NETRESOURCE_LOCAL_NAME, 
    NETRESOURCE_REMOTE_NAME, 
    NETRESOURCE_COMMENT, 
    NETRESOURCE_PROVIDER, 
$ 
 
--** 
-- Allocate a NETRESOURCE structure. 
-- 
function new_NETRESOURCE( atom dwScope, atom dwType, atom dwDisplayType, atom dwUsage, sequence szLocalName = "", 
        sequence szRemoteName = "", sequence szComment = "", sequence szProvider = "" ) 
     
    integer nBufferSize, nOffset 
    atom lpNetResource, lpLocalName, lpRemoteName, lpComment, lpProvider 
     
    -- calculate the total buffer size 
    nBufferSize = SIZEOF_NETRESOURCE + 4 -- four NULL terminators 
                    + length( szLocalName ) 
                    + length( szRemoteName ) 
                    + length( szComment ) 
                    + length( szProvider ) 
     
    -- allocate the entire buffer 
    lpNetResource = allocate_data( nBufferSize ) 
    mem_set( lpNetResource, NULL, nBufferSize ) 
     
    nOffset = SIZEOF_NETRESOURCE 
    if length( szLocalName ) = 0 then 
        lpLocalName = NULL 
    else 
        lpLocalName = lpNetResource + nOffset 
        poke( lpLocalName, szLocalName & NULL ) 
    end if 
     
    nOffset += length( szLocalName ) + 1 
    if length( szRemoteName ) = 0 then 
        lpRemoteName = NULL 
    else 
        lpRemoteName = lpNetResource + nOffset 
        poke( lpRemoteName, szRemoteName & NULL ) 
    end if 
     
    nOffset += length( szRemoteName ) + 1 
    if length( szComment ) = 0 then 
        lpComment = NULL 
    else 
        lpComment = lpNetResource + nOffset 
        poke( lpComment, szComment & NULL ) 
    end if 
     
    nOffset += length( lpComment ) + 1 
    if length( szProvider ) = 0 then 
        lpProvider = NULL 
    else 
        lpProvider = lpNetResource + nOffset 
        poke( lpProvider, szProvider & NULL ) 
    end if 
     
    poke4( lpNetResource + NETRESOURCE__dwScope,        dwScope ) 
    poke4( lpNetResource + NETRESOURCE__dwType,         dwType ) 
    poke4( lpNetResource + NETRESOURCE__dwDisplayType,  dwDisplayType ) 
    poke4( lpNetResource + NETRESOURCE__dwUsage,        dwUsage ) 
    poke4( lpNetResource + NETRESOURCE__lpLocalName,    lpLocalName ) 
    poke4( lpNetResource + NETRESOURCE__lpRemoteName,   lpRemoteName ) 
    poke4( lpNetResource + NETRESOURCE__lpComment,      lpComment ) 
    poke4( lpNetResource + NETRESOURCE__lpProvider,     lpProvider ) 
     
    return lpNetResource 
end function 
 
--** 
-- Read a NETRESOURCE structure from memory. 
-- 
function get_NETRESOURCE( atom lpNetResource, integer nCount = 1 ) 
     
    sequence data = repeat( {}, nCount ) 
    atom offset = 0 
     
    for i = 1 to nCount do 
         
        -- peek the whole structure (eight 4-byte values) 
        data[i] = peek4u({ lpNetResource + offset, 8 }) 
         
        -- values 5-8 are really strings 
        for j = 5 to 8 do 
             
            if data[i][j] = 0 then 
                -- null (empty) string 
                data[i][j] = "" 
            else 
                -- peek the string value 
                data[i][j] = peek_string( data[i][j] ) 
            end if 
             
        end for 
         
        -- increment the offset 
        offset += SIZEOF_NETRESOURCE 
         
    end for 
     
    return data 
end function 
 
--** 
-- Translate an error code into a descriptive string. 
-- 
public function get_net_error( integer error ) 
     
    sequence string 
     
    switch error do 
         
        case NO_ERROR then 
            string = "success" 
             
        case ERROR_INVALID_HANDLE then 
            string = "invalid handle" 
             
        case ERROR_INVALID_PARAMETER then 
            string = "invalid parameter" 
             
        case ERROR_MORE_DATA then 
            string = "more data available" 
             
        case ERROR_NO_MORE_ITEMS then 
            string = "no more items" 
             
        case ERROR_INVALID_ADDRESS then 
            string = "invalid address" 
             
        case ERROR_NO_NET_OR_BAD_PATH then 
            string = "no network or bad path" 
             
        case ERROR_NOT_CONTAINER then 
            string = "not a container" 
             
        case ERROR_EXTENDED_ERROR then 
            string = "extended error" 
             
        case ERROR_NO_NETWORK then 
            string = "no network available" 
             
        case else 
            string = "unknown error" 
             
         
    end switch 
     
    return "Error: " & string 
end function 
 
--** 
-- Scan the network for available resources. 
-- 
-- dwScope can be one of the following: 
-- 
--   RESOURCE_CONNECTED     Enumerate all currently connected resources. The function ignores the dwUsage parameter. 
-- 
--   RESOURCE_CONTEXT       Enumerate only resources in the network context of the caller. Specify this 
--                          value for a Network Neighborhood view. The function ignores the dwUsage parameter. 
-- 
--   RESOURCE_GLOBALNET     Enumerate all resources on the network. 
-- 
--   RESOURCE_REMEMBERED    Enumerate all remembered (persistent) connections. The function ignores the dwUsage parameter. 
-- 
-- dwType can be one of the following: 
-- 
--   RESOURCETYPE_ANY       All resources. This value cannot be combined with RESOURCETYPE_DISK or RESOURCETYPE_PRINT. 
-- 
--   RESOURCETYPE_DISK      All disk resources. 
-- 
--   RESOURCETYPE_PRINT     All print resources. 
-- 
-- dwUsage can be one of the following: 
-- 
--   0                          All resources. 
-- 
--   RESOURCEUSAGE_CONNECTABLE  All connectable resources. 
-- 
--   RESOURCEUSAGE_CONTAINER    All container resources. 
-- 
--   RESOURCEUSAGE_ATTACHED     Setting this value forces WNetOpenEnum to fail if the user is not authenticated. 
--                              The function fails even if the network allows enumeration without authentication. 
-- 
--   RESOURCEUSAGE_ALL          Setting this value is equivalent to setting RESOURCEUSAGE_CONNECTABLE,  
--                              RESOURCEUSAGE_CONTAINER, and RESOURCEUSAGE_ATTACHED. 
-- 
public function scan_network( atom dwScope = RESOURCE_CONTEXT, atom dwType = RESOURCETYPE_ANY, atom dwUsage = 0 ) 
     
    atom lpNetResource = NULL         -- Scan the entire network. 
    atom lphEnum = allocate_data( 4 ) -- Pointer to an enumeration handle. 
    object hResult = NO_ERROR 
     
    hResult = WNetOpenEnum( dwScope, dwType, dwUsage, lpNetResource, lphEnum ) 
    if hResult != NO_ERROR then 
        -- WNetOpenEnum() failed. 
        return hResult 
    end if 
     
    atom hEnum = peek4u( lphEnum ) -- Get the enumeration handle. 
    free( lphEnum ) 
     
    integer nBufferSize = 16 * KB -- N.B. MS docs recommend 16KB here. 
     
    atom lpcCount = allocate_data( 4 ) 
    poke4( lpcCount, -1 ) -- Get as many entries as possible. 
     
    atom lpBuffer = allocate_data( nBufferSize ) 
    atom lpBufferSize = allocate_data( 4 ) 
    poke4( lpBufferSize, nBufferSize ) 
     
    hResult = WNetEnumResource( hEnum, lpcCount, lpBuffer, lpBufferSize ) 
    while hResult = ERROR_MORE_DATA do 
        -- We need to allocate more memory. 
         
        nBufferSize += 16 * KB 
        poke4( lpBufferSize, nBufferSize ) 
         
        free( lpBuffer ) 
        lpBuffer = allocate_data( nBufferSize ) 
         
        hResult = WNetEnumResource( hEnum, lpcCount, lpBuffer, lpBufferSize ) 
    end while 
     
    if hResult = NO_ERROR then 
         
        -- Get the NETRESOURCE items. 
        integer nCount = peek4u( lpcCount ) 
        hResult = get_NETRESOURCE( lpBuffer, nCount ) 
         
    elsif hResult = ERROR_NO_MORE_ITEMS then 
         
        -- Return an empty set. 
        hResult = {} 
         
    end if 
     
    free( lpBufferSize ) 
    free( lpBuffer ) 
    free( lpcCount ) 
     
    WNetCloseEnum( hEnum ) 
     
    return hResult 
end function 
 
--** 
-- Enumerate the shares provided by server at szRemoteName 
-- 
-- dwType can be one of the following: 
-- 
--   RESOURCETYPE_ANY       All resources. This value cannot be combined with RESOURCETYPE_DISK or RESOURCETYPE_PRINT. 
-- 
--   RESOURCETYPE_DISK      All disk resources. 
-- 
--   RESOURCETYPE_PRINT     All print resources. 
-- 
-- dwUsage can be one of the following: 
-- 
--   0                          All resources. 
-- 
--   RESOURCEUSAGE_CONNECTABLE  All connectable resources. 
-- 
--   RESOURCEUSAGE_CONTAINER    All container resources. 
-- 
--   RESOURCEUSAGE_ATTACHED     Setting this value forces WNetOpenEnum to fail if the user is not authenticated. 
--                              The function fails even if the network allows enumeration without authentication. 
-- 
--   RESOURCEUSAGE_ALL          Setting this value is equivalent to setting RESOURCEUSAGE_CONNECTABLE,  
--                              RESOURCEUSAGE_CONTAINER, and RESOURCEUSAGE_ATTACHED. 
-- 
public function enumerate_shares( sequence szRemoteName, atom dwType = RESOURCETYPE_ANY, atom dwUsage = 0 ) 
     
    atom dwScope = RESOURCE_GLOBALNET -- Enumerate all resources on the server. 
    atom lpNetResource = new_NETRESOURCE( dwScope, dwType, NULL, dwUsage, "", szRemoteName, "", "" ) 
     
    atom lphEnum = allocate_data( 4 ) -- Pointer to an enumeration handle. 
    object hResult = NO_ERROR 
     
    hResult = WNetOpenEnum( dwScope, dwType, dwUsage, lpNetResource, lphEnum ) 
    if hResult != NO_ERROR then 
        -- WNetOpenEnum() failed. 
        return hResult 
    end if 
     
    atom hEnum = peek4u( lphEnum ) -- Get the enumeration handle. 
    free( lphEnum ) 
     
    integer nBufferSize = 16 * KB -- N.B. MS docs recommend 16KB here. 
     
    atom lpcCount = allocate_data( 4 ) 
    poke4( lpcCount, -1 ) -- Get as many entries as possible. 
     
    atom lpBuffer = allocate_data( nBufferSize ) 
    atom lpBufferSize = allocate_data( 4 ) 
    poke4( lpBufferSize, nBufferSize ) 
     
    hResult = WNetEnumResource( hEnum, lpcCount, lpBuffer, lpBufferSize ) 
    while hResult = ERROR_MORE_DATA do 
        -- We need to allocate more memory. 
         
        nBufferSize += 16 * KB 
        poke4( lpBufferSize, nBufferSize ) 
         
        free( lpBuffer ) 
        lpBuffer = allocate_data( nBufferSize ) 
         
        hResult = WNetEnumResource( hEnum, lpcCount, lpBuffer, lpBufferSize ) 
    end while 
     
    if hResult = NO_ERROR then 
         
        -- Get the NETRESOURCE items. 
        integer nCount = peek4u( lpcCount ) 
        hResult = get_NETRESOURCE( lpBuffer, nCount ) 
         
    elsif hResult = ERROR_NO_MORE_ITEMS then 
         
        -- Return an empty set. 
        hResult = {} 
         
    end if 
     
    free( lpNetResource ) 
    free( lpBufferSize ) 
    free( lpBuffer ) 
    free( lpcCount ) 
     
    WNetCloseEnum( hEnum ) 
     
    return hResult 
end function 

Example:

include std/error.e 
include shares.e 
 
object network = scan_network() 
if atom( network ) then 
    crash( get_net_error(network) ) 
end if 
 
for i = 1 to length( network ) do 
     
    sequence remoteName = network[i][NETRESOURCE_REMOTE_NAME] 
    if length( remoteName ) then 
        printf( 1, "%s\n", {remoteName} ) 
    else 
        -- empty remote name is the "Network" container 
        puts( 1, "Network Neighborhood\n" ) 
    end if 
     
    object shares = enumerate_shares( remoteName ) 
    if atom( shares ) then 
        -- you could also just 'continue' here 
        crash( get_net_error(shares) ) 
    end if 
     
    for j = 1 to length( shares ) do 
         
        sequence shareName = shares[j][NETRESOURCE_REMOTE_NAME] 
        printf( 1, "\t%s\n", {shareName} ) 
         
    end for 
     
end for 

-Greg

new topic     » goto parent     » topic index » view message » categorize

11. Re: Detecting shares using walk_dir()

This is exactly what I'm looking for; thank you for excellent work!

Will this be part of the euphoria includes?

Regards Kenneth aka ZNorQ.

new topic     » goto parent     » topic index » view message » categorize

Search



Quick Links

User menu

Not signed in.

Misc Menu