Ruby
SharePoint

sharepointにファイルをアップロードする

経緯

office365のsharepointへ、サーバからファイルをアップロードする要件が必要になってきました。
webdav使えば簡単にアップロードできるかなーと安易に考えていたのですが、UNIX系OSのwebdavからつなぐ方法が見つからず……。

結局、curl コマンド ライン ツールで SharePoint Online REST API を呼び出すを参考にrubyで専用のスクリプトを組みました。
REST APIの認証使おうとしたんですが、ブラウザ経由しない分、こちらの方が使い安いですね。

やっていること

  1. SharePoint Online へリクエストを行い RpsContextCookie を取得
  2. Office 365 の STS へ SAML リクエストを行い、BinarySecurityTokenを取得
  3. BinarySecurityTokenを使ってSharePoint Online へ認証リクエストを行ない、Authentication Cookieを取得
  4. /_api/contextinfoにアクセスしてFormDigestValueを取得
  5. FormDigestValueを使って、ファイルをPOSTする

1-3はcurl コマンド ライン ツールで SharePoint Online REST API を呼び出すにある通りです。
最初、FormDigestValueが足りなくて、403系のエラーがでまくって、ドツボにはまりました。

作ったスクリプト

sharepoint_upload.rb
#!/usr/bin/env ruby

# SHAREPOINT にファイルアップロードするためのスクリプトです
# 環境変数に予め以下の3つを設定ください
# SHAREPOINT_DOMAIN
# USERNAME
# PASSWORD
#
# 以下の様なコマンドで利用を想定しています
# ./sharepoint_upload.rb local_filepath upload_path upload_filename

require "base64"
require "net/https"
require "pp"
require "uri"
require "json"

class SharepointUpload

  SHAREPOINT_DOMAIN = ENV["SHAREPOINT_DOMAIN"]
  USERNAME = ENV["USERNAME"]
  PASSWORD = ENV["PASSWORD"]

  def get_cookie(response)
    cookie = {}
    response.get_fields("Set-Cookie").each do |str|
      k,v = str[0...str.index(";")].split("=", 2)
      cookie[k] = v
    end
    cookie
  end

  @cookie = nil
  def login_cookie_get
    return @cookie if @cookie
    http = Net::HTTP.new(SHAREPOINT_DOMAIN, 443)
    http.use_ssl = true
    http.verify_mode = OpenSSL::SSL::VERIFY_NONE
    response = http.start{|https|
      http.post("/_layouts/Authenticate.aspx?Source=", "")
    }

    cookie = get_cookie(response)
    xml = "<s:Envelope xmlns:s='http://www.w3.org/2003/05/soap-envelope' xmlns:a='http://www.w3.org/2005/08/addressing' xmlns:u='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd'><s:Header><a:Action s:mustUnderstand='1'>http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action><a:ReplyTo><a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address></a:ReplyTo><a:To s:mustUnderstand='1'>https://login.microsoftonline.com/extSTS.srf</a:To><o:Security s:mustUnderstand='1' xmlns:o='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd'><o:UsernameToken><o:Username>#{USERNAME}</o:Username><o:Password>#{PASSWORD}</o:Password></o:UsernameToken></o:Security></s:Header><s:Body><t:RequestSecurityToken xmlns:t='http://schemas.xmlsoap.org/ws/2005/02/trust'><wsp:AppliesTo xmlns:wsp='http://schemas.xmlsoap.org/ws/2004/09/policy'><a:EndpointReference><a:Address>https://tenant.sharepoint.com/</a:Address></a:EndpointReference></wsp:AppliesTo><t:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</t:KeyType><t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType><t:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</t:TokenType></t:RequestSecurityToken></s:Body></s:Envelope>"
    http = Net::HTTP.new("login.microsoftonline.com", 443)
    http.use_ssl = true
    req = Net::HTTP::Post.new("/extSTS.srf")
    req["Cookie"] = cookie.map{|k,v| "#{k}=#{v}" }.join(";")
    req.body = xml
    response = http.request(req)
    response.body =~ /wsse:BinarySecurityToken Id="Compact0">([^<]*)</
    binary_security_token = $1

    http = Net::HTTP.new(SHAREPOINT_DOMAIN, 443)
    http.use_ssl = true
    http.verify_mode = OpenSSL::SSL::VERIFY_NONE
    req = Net::HTTP::Post.new("/_forms/default.aspx?wa=wsignin1.0")
    req.body = binary_security_token
    response = http.request(req)
    @cookie = get_cookie(response)
  end

  def post(path, headers ={}, body = nil)
    http = Net::HTTP.new(SHAREPOINT_DOMAIN, 443)
    http.use_ssl = true

    cookie = login_cookie_get
    req = Net::HTTP::Post.new(path)
    headers.merge({"Cookie" => cookie.map{|k,v| "#{k}=#{v}" }.join("; ")}).each do | k, v|
      req[k] = v
    end
    req.body = body if body
    http.request(req)
  end

  def form_digest_value
    response = post("/_api/contextinfo", "Accept" => "application/json;")
    JSON.parse(response.body)["FormDigestValue"]
  end

  def initialize(upload_file, upload_path, upload_filename)
    unless File.exists?(upload_file)
      puts "File not found"
      return
    end
    path = "/_api/web/GetFolderByServerRelativeUrl('#{URI.encode(upload_path)}')/Files/add(url='#{URI.encode(File.basename(upload_filename))}',overwrite=true)"

    body = open(upload_file).read
    header = {"X-RequestDigest" => form_digest_value, "content-length" => body.length}
    response = post(path, header, body)
    puts response.body
  end
end

upload_file = ARGV[0]
upload_path = ARGV[1]
upload_filename = ARGV[2]

SharepointUpload.new(upload_file, upload_path, upload_filename)