summaryrefslogtreecommitdiffstats
path: root/scripts/daap-get-library.nse
blob: a39fca41a07065ee252b927e2ea9c323dd5991d3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
local http = require "http"
local nmap = require "nmap"
local shortport = require "shortport"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"

description = [[
Retrieves a list of music from a DAAP server. The list includes artist
names and album and song titles.

Output will be capped to 100 items if not otherwise specified in the
<code>daap_item_limit</code> script argument. A
<code>daap_item_limit</code> below zero outputs the complete contents of
the DAAP library.

Based on documentation found here:
http://www.tapjam.net/daap/.
]]

---
-- @args daap_item_limit Changes the output limit from 100 songs. If set to a negative value, no limit is enforced.
--
-- @output
-- | daap-get-library:
-- |   BUBBA|TWO
-- |     Fever Ray
-- |       Fever Ray (Deluxe Edition)
-- |         Concrete Walls
-- |         I'm Not Done
-- |         Here Before
-- |         Now's The Only Time I Know
-- |         Stranger Than Kindness
-- |         Dry And Dusty
-- |         Keep The Streets Empty For Me
-- |         Triangle Walks
-- |         If I Had A Heart
-- |         Seven
-- |         When I Grow Up
-- |_        Coconut

author = "Patrik Karlsson"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"discovery", "safe"}


-- Version 0.2
-- Created 01/14/2010 - v0.1 - created by Patrik Karlsson
-- Revised 01/23/2010 - v0.2 - changed to port_or_service, added link to documentation, limited output to 100 songs or to daap_item_limit script argument.

portrule = shortport.port_or_service(3689, "daap")

--- Gets the name of the library from the server
--
-- @param host table containing an ip field.
-- @param port table containing number and protocol fields.
-- @return string containing the name of the library
function getLibraryName( host, port )
  local libname, pos
  local url = "daap://" .. host.ip .. "/server-info"
  local response = http.get( host, port, url, nil, nil, nil)

  if response == nil or response.body == nil or response.body=="" then
    return
  end

  pos = string.find(response.body, "minm")

  if pos > 0 then
    pos = pos + 4
    libname, pos = string.unpack( ">s4", response.body, pos )
  end

  return libname
end

--- Reads the first item value specified by name
--
-- @param data string containing the unparsed item
-- @param name string containing the name of the value to read
-- @return number
local function getAttributeAsInt( data, name )

  local pos = string.find(data, name)
  local attrib

  if pos and pos > 0 then
    pos = pos + 4
    local len
    len, pos = string.unpack( ">I4", data, pos )

    if ( len ~= 4 ) then
      stdnse.debug1("Unexpected length returned: %d", len )
      return
    end

    attrib, pos = string.unpack( ">I4", data, pos )
  end

  return attrib

end

--- Gets the revision number for the library
--
-- @param host table containing an ip field.
-- @param port table containing number and protocol fields.
-- @return number containing the session identity received from the server
function getSessionId( host, port )

  local sessionid
  local response = http.get( host, port, "/login", nil, nil, nil )

  if response ~= nil then
    sessionid = getAttributeAsInt( response.body, "mlid")
  end

  return sessionid
end

--- Gets the revision number for the library
--
-- @param host table containing an ip field.
-- @param port table containing number and protocol fields.
-- @param sessionid number containing session identifier from <code>getSessionId</code>
-- @return number containing the revision number for the library
function getRevisionNumber( host, port, sessionid )
  local url = "/update?session-id=" .. sessionid .. "&revision-number=1"
  local revision
  local response = http.get( host, port, url, nil, nil, nil )

  if response ~= nil then
    revision = getAttributeAsInt( response.body, "musr")
  end

  return revision
end

--- Gets the database identity for the library
--
-- @param host table containing an ip field.
-- @param port table containing number and protocol fields.
-- @param sessionid number containing session identifier from <code>getSessionId</code>
-- @param revid number containing the revision id as retrieved from <code>getRevisionNumber</code>
function getDatabaseId( host, port, sessionid, revid )
  local url = "/databases?session-id=" .. sessionid .. "&revision-number=" .. revid
  local response = http.get( host, port, url, nil, nil, nil )
  local miid

  if response ~= nil then
    miid = getAttributeAsInt( response.body, "miid")
  end

  return miid
end

--- Gets a string item type from data
--
-- @param data string starting with the 4-bytes of length
-- @param pos number containing offset into data
-- @return pos number containing new position after reading string
-- @return value string containing the string item that was read
local function getStringItem( data, pos )
  local item, pos = string.unpack(">s4", data, pos)
  return pos, item
end

local itemFetcher = {}

itemFetcher["mikd"] = function( data, pos )  return getStringItem( data, pos ) end
itemFetcher["miid"] = itemFetcher["mikd"]
itemFetcher["minm"] = itemFetcher["mikd"]
itemFetcher["asal"] = itemFetcher["mikd"]
itemFetcher["asar"] = itemFetcher["mikd"]

--- Parses a single item (mlit)
--
-- @param data string containing the unparsed item starting at the first available tag
-- @param len number containing the length of the item
-- @return item table containing <code>mikd</code>, <code>miid</code>, <code>minm</code>,
-- <code>asal</code> and <code>asar</code> when available
parseItem = function( data, len )

  local pos, name, value = 1, nil, nil
  local item = {}

  while( len - pos > 0 ) do
    name, pos = string.unpack( "c4", data, pos )

    if itemFetcher[name] then
      pos, item[name] = itemFetcher[name](data, pos )
    else
      stdnse.debug1("No itemfetcher for: %s", name)
      break
    end

  end

  return item

end

--- Request and process all music items
--
-- @param host table containing an ip field.
-- @param port table containing number and protocol fields.
-- @param sessionid number containing session identifier from <code>getSessionId</code>
-- @param dbid number containing database id from <code>getDatabaseId</code>
-- @param limit number containing the maximum amount of songs to return
-- @return table containing the following structure [artist][album][songs]
function getItems( host, port, sessionid, revid, dbid, limit )
  local meta = "dmap.itemid,dmap.itemname,dmap.itemkind,daap.songalbum,daap.songartist"
  local url = "/databases/" .. dbid .. "/items?type=music&meta=" .. meta .. "&session-id=" .. sessionid .. "&revision-number=" .. revid
  local response = http.get( host, port, url, nil, nil, nil )
  local item, data, pos, len
  local items = {}
  local limit = limit or -1

  if response == nil then
    return
  end

  -- get our position to the list of items
  pos = string.find(response.body, "mlcl")
  pos = pos + 4

  while ( pos > 0 and pos + 8 < response.body:len() ) do

    -- find the next single item
    pos = string.find(response.body, "mlit", pos)
    pos = pos + 4

    len, pos = string.unpack( ">I4", response.body, pos )

    if ( pos < response.body:len() and pos + len < response.body:len() ) then
      data, pos = string.unpack( "c" .. len, response.body, pos )
    else
      break
    end

    -- parse a single item
    item = parseItem( data, len )

    local album = item.asal or "unknown"
    local artist= item.asar or "unknown"
    local song  = item.minm or ""

    if items[artist] == nil then
      items[artist] = {}
    end

    if items[artist][album] == nil then
      items[artist][album] = {}
    end

    if limit == 0 then
      break
    elseif limit > 0 then
      limit = limit - 1
    end

    table.insert( items[artist][album], song )

  end


  return items

end


action = function(host, port)

  local limit = tonumber(nmap.registry.args.daap_item_limit) or 100
  local libname = getLibraryName( host, port )

  if libname == nil then
    return
  end

  local sessionid = getSessionId( host, port )

  if sessionid == nil then
    return stdnse.format_output(true, "Libname: " .. libname)
  end

  local revid = getRevisionNumber( host, port, sessionid )

  if revid == nil then
    return stdnse.format_output(true, "Libname: " .. libname)
  end

  local dbid = getDatabaseId( host, port, sessionid, revid )

  if dbid == nil then
    return
  end

  local items = getItems( host, port, sessionid, revid, dbid, limit )

  if items == nil then
    return
  end

  local albums, songs, artists, results = {}, {}, {}, {}

  table.insert( results, libname )

  for artist, v in pairs(items) do
    albums = {}
    for album, v2 in pairs(v) do
      songs = {}
      for _, song in pairs( v2 ) do
        table.insert( songs, song )
      end
      table.insert( albums, album )
      table.insert( albums, songs )
    end
    table.insert( artists, artist )
    table.insert( artists, albums )
  end

  table.insert( results, artists )
  local output = stdnse.format_output( true, results )

  if limit > 0 then
    output = output .. string.format("\n\nOutput limited to %d items", limit )
  end

  return output

end