経緯
office365のsharepointへ、サーバからファイルをアップロードする要件が必要になってきました。
webdav使えば簡単にアップロードできるかなーと安易に考えていたのですが、UNIX系OSのwebdavからつなぐ方法が見つからず……。
結局、curl コマンド ライン ツールで SharePoint Online REST API を呼び出すを参考にrubyで専用のスクリプトを組みました。
REST APIの認証使おうとしたんですが、ブラウザ経由しない分、こちらの方が使い安いですね。
やっていること
- SharePoint Online へリクエストを行い RpsContextCookie を取得
- Office 365 の STS へ SAML リクエストを行い、BinarySecurityTokenを取得
- BinarySecurityTokenを使ってSharePoint Online へ認証リクエストを行ない、Authentication Cookieを取得
- /_api/contextinfoにアクセスしてFormDigestValueを取得
- 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)